Pull to refresh

Comments 44

Отличная статья. У меня был свой сервис url shortener в 2006. Пришлось прикрыть: ссылки на порно через него делали добрые посетители)

Статья действительно большая, но интересная!

Небольшие замечания:

  • откажитесь от taskfile, makefile имеет значительно меньше ограничений и больше функционала

  • вынесите migrations/ из internal/, это прям ужасно, увидел бы такое на работе, засомневался бы в компетенциях разработчика...

  • убейте slog логгер, он сырой и настолько технологично далек от других oss логеров что не описать, перевезите все на zap, только на zap.

Спасибо, рад что понравилось :)

  • Возможно, makefile гибче, но мне больше нравится формат и синтаксис taskfile, и я пока не упирался ни разу в предел его возможностей

  • Миграции у меня не в internal, а в корне лежат. Похоже, вы ошиблись. Ну или у меня где-то опечатка (где?)

  • Мне slog очень понравился, и в него можно обернуть и тот же zap при желании. В видео я объяснял - slog хорош тем, что он универсален. Если использовать zap, то будет очень сложно в будущем переехать на что-то другое.

  • откажитесь от taskfile, makefile имеет значительно меньше ограничений и больше функционала

Какая-то мода на Taskfile пошла. Я пока так и не понял прикола. Тулза, которую надо ставить отдельно, снова эти YAML-конфиги.

а make разве не надо "отдельно ставить"?

У task, в отличие от make, есть огромный плюс - он максимально кроссплатформенный засчет того, что, во-первых, тоже написан на go, а во-вторых - имеет внутри bash интерпретатор, засчет чего скрипты одинаково работают на всех платформах, включая винду.

А JWT  где передается в хэдерах в Cookie? Если так то у браузеров ограничение на 1024 символа. Изза ограничений размера, JWT  хранит ОЧЕНЬ много оверхэд информации. Не легче просто взять сериализовать JSON и зашифровать? получится тоже самое, но в 100 раз короче и поместить больше полей данных если будут JSON расширяться.

В https://www.ietf.org/rfc/rfc2109.txt §6.3 говорит, что должно храниться хотя бы 4096 байт. Есть сайт со статистикой (на 2013 год, можно запустить проверку для своего браузера), где показано, что все браузеры примерно так и ограничивают максимальный размер Cookie. http://browsercookielimits.iain.guru/#limits

Вообще, мне казалось, что чтобы JWT разросся до 4Кб, туда нужно слишком много информации положить.

Подскажите, для этой таблицы/столбца SQLite разве не создаёт сразу уникальный индекс?

email TEXT NOT NULL UNIQUE,

Там просто ниже по коду миграции создаётся ещё один (неуникальный) индекс idx_email на этом столбце.

Спасибо, добавил в статью

Так у меня и используется "log/slog"

golang.org/x/exp/slog я использовал в прошлой статье, тогда еще не было 1.21

у меня все ходы записаны

Ну так это в исходниках) Где-то недоглядел, да. Копипастил из предыдущего проекта.

Но в статье такого нет.

Рекуррентно заходить в поддиректории оно, конечно, в таком виде не будет — это сделать немного сложнее.

Просто добавить две звёздочки для подпапок:

$ protoc -I proto proto/sso/**/*.proto --go_out=./gen/go/ --go_opt=paths=source_relative --go-grpc_out=./gen/go/ --go-grpc_opt=paths=source_relative

На Маке работает, если запускать команду в терминале.

Но если запускать команду внутри go_task, то ругается на "Could not make proto path relative: proto/sso/**/*.proto: No such file or directory"

Возможно, проблема связана с тем, как go_task обрабатывает символы подстановки, такие как **.

Потому для Taskfile.yaml пришлось применить вот такой вариант:

- mkdir -p gen/go & find ./proto/sso -name '*.proto' | xargs protoc -I proto --go_out=./gen/go/ --go_opt=paths=source_relative --go-grpc_out=./gen/go/ --go-grpc_opt=paths=source_relative

По поводу первого варианта - я тоже думаю, что проблема на стороне task. Но если сделать вот так, то будет работать и через него:

protoc -I proto proto/**/*.proto --go_out=./gen/go/ --go_opt=paths=source_relative --go-grpc_out=./gen/go/ --go-grpc_opt=paths=source_relative

Тут я убрал sso из пути, указал общий путь до protos. Такой вариант и в целом правильней, т.к. помимо sso у вас тут могут лежать контракты и для других сервисов.

// TODO: инициализировать объект конфига
// TODO: инициализировать логгер

Лучше бы наоборот. Иначе мы не увидим в логе ошибки инициализации конфига. Но, тут возможна проблема курицы и яйца, т.к. в конфиге определяем, куда складывать логи. Вывод: нужно воспользоваться переменными окружения для конфигурирования логов.

Ошибки могут возникнуть и при работе с переменными окружения - они могут окзаться пустыми и не валидными. Это частично решает проблему, конечно, но на мой взгляд, эти усилия того не стоят.

Ошибки инициализации конфига в логах нам и не нужны, т.к. наше приложение без конфига вообще не запустится, а такое поведение тоже нужно уметь мониторить.

/config/config_local.yaml хорошо бы прописать в .gitignore

Если и добавлять такое в игнор, то я бы посоветовал .git/info/exclude, тогда не придется коммитить это вместе с .gitignore

Иначе каждый разработчик проекта будет коммитить в gitignore свои личные локальные файлы.

type My struct{}

func (My) Try() {
	println("My")
}

type My2 struct {
	My
}

func (My2) Try() {
	println("My2")
}

func main() {
	My2{}.Try() // My2
}


Понял, что для структуры serverAPI можно перезаписать методы из встроенной UnimplementedAuthServer. Но зачем прослойка в виде интерфейса Auth, можно же сразу реализовывать интерфейс AuthServer внутри services/auth?

Сервер валидирует и тупо перекладывает данные и ошибки в слой бизнес-логики (services). Но валидация - часть бизнес-логики, а перекладывать данные и ошибки - вообще сомнительное занятие.

Интерфейс Auth - это интерфейс для сервисного слоя, не для gRPC-сервера. То есть, это уже бизнес-логика, а не хэндлеры.

Интерфейсы UserSaver и UserProvider реализованы в Storage (как и в предыдущей статье), но тогда контекст можно вынести туда, а не таскать его каждый раз в методах SaveUser() и User(). Или я что-то не учитываю?

ведь кто сказал, что за сохранение и получение пользователей обязана отвечать одна система?

Нашёл ответ, зачем таскать контексты в каждом методе. Попахивает "предварительной оптимизацией". Даже если предположить, что источники данных для каждого интерфейса могут быть разные, у них вполне себе может быть общий слой Storage (в котором находится базовый контекст).

я обычно размещаю их в месте использования, а не рядом с реализацией

Согласен. Можно легко и убедительно обосновать. Но я бы оставлял в комментариях реализуемых методов, что это реализация такого-то интерфейса. Поможет поддерживать код?

Ни разу не встречался с подобными комментариями в рабочих проектах, и проблемой это не становилось. Обычно IDE с этим справляется неплохо.

Как применяется claims - не очень понятно, я бы исправил.

Текущая реализация метода имеет одну критичную дыру в безопасности — он не защищен от брутфорса (перебора паролей).

Это внешняя инфраструктурная задача относительно функциональности. Мы же не пихаем в каждый веб-сервис защиту от DDOS-атак. Например, поможет fail2ban

Мы же не пихаем в каждый веб-сервис защиту от DDOS-атак.

Перегрузить сервис может и обычный клиент. Например случайно отправив 100500 запросов. Поэтому добавление хоть какого-то ограничения является хорошей практикой, КМК.

Мы используем у себя https://github.com/go-chi/httprate, она совместима с нормальными middleware, поэтому можно использовать в любых стандартных роутерах.

gRPC пока не ограничиваем.

Смущает двойная обработка ошибок, например в Login. Зачем их там логировать? Напрашивается отдельный middleware-слой на потоке данных, в котором выполняется централизованное логирование.

func (s *Storage) Stop() error {
	return s.db.Close()
}

Номинально присутствует метод для остановки Storage, но он не применяется для Graceful Shutdown.

Было бы удобней, если бы вы писали все подобные комментарии в одной ветке, таким образом сгруппировав их. То есть, в ответе к какому-то из своих предыдущих комментариев.

Иначе среди ваших комментариев затеряются те, что от других людей :)

Это я без наезда, если что :) Просто просьба с моей стороны, чтобы проще было ориентироваться в комментариях. Группировка внутри одной ветки помогла бы.

За поправки к статье я благодарен, в любом случае. У вас они, как правило, по делу, но я не всегда успеваю отвечать.

Просто хочу сказать Спасибо за такой прекрасный материал, именно таких подробных статей иногда не хватает для изучения нужных технологий, ещё раз спасибо!

Ещё бы знать, что авторизация (от authority, буквально: уполномочивание) -- это и есть та самая "работа с пермишнами". А то, что в этой статье называется этим словом -- аутентификация (от authenticity, буквально: проверка подлинности).

И по этой же причине понятие "авторизоваться" (то есть, уполномочить себя) не имеет смысла.

И да, использование правильной терминологии -- важно и позволяет сэкономить время при обсуждении. По той причине, что нередко за эти две задачи отвечают разные независимые компоненты. И то, что проблемы с аутентификацией и авторизацией решаются разными способами.

Спасибо Вам за статью! Хочу поделиться двумя предложениями, которые могли бы улучшить читаемость пары строк кода.

Вместо `signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT)` мне на практике оказалось гораздо удобнее использовать `signal.NotifyContext(ctx, syscall.SIGTERM, syscall.SIGINT)`. Он не заставляет создавать лишние каналы снаружи) Он не сразу в пакете signal появился, поэтому про него часто не знают.

Для проверки диапазона времени можно использовать `require.WithinDuration(loginTime.Add(s.Config.TokenTTL), claims.ExpiresAt.Time, 1*time.Second)`. Должно выглядеть "прозрачнее" для читателя.

Спасибо Вам за статью!
Но чтобы было прям супер ультимативно - не хватает закрутки всего этого дела в docker. А там можно и postges поднять. Хоть это и пет проект, но что сейчас стоит в 2023 году уже почти 2024 запустить postgres в докере?
Я если честно думал, что SQLite доживает последние дни, хотя он как PHP - всё не сдаётся.

А что нам даст наличие postgres вместо sqlite? Да, postgres поставить и использовать не супер сложно, но, всё же, её надо будет:

  • Завести: описать в docker-compose, в конфиге приложения, в целом правильно сконфигурировать

  • Запустить: обязательно придется иметь докер на удалённой машине

  • Поддерживать и мониторить: если упадёт, то приложение сломается

  • Отдавать чуть больше ресурсов: повышаются минимальные требования к серверу

  • Сложнее делать дампы и бэкапы: в случае sqlite достаточно просто скопировать один файл

Это навскидку, список можно продолжить. Проблемы не критичные, конечно, но какой профит наличие postgres даёт взамен? При условии, что у нас пет-проект.

Я бы сказал даже так, sqlite это не компромисс в стиле "для пет-проекта сойдёт", это наиболее предпочтительный вариант. Не нужно усложнять себе жизнь раньше времене. При необходимости вы всегда сможете переехать на любую другую СУБД - наш код позволяет это сделать без проблем, достаточно лишь написать новую реализацию Storage.

Ну не знаю. То что Ты описываешь, делается за 2 минуты. Работает на любом "разбери и спаяй". Один раз путь пройти длинной в пару часов и тиражируй себе на здоровье.

Хотя для кого-то может и гвоздь забить да полку повесить - это целый челлендж. О чем это я... Старею видимо. Многие простые вещи, уже не простые оказывается.

На один китайский одноплатник недавно навешал 50 докеров. Там Хом ассистент, постгрес и куча всего ещё. И всё едет и не ломается. Вот мне и странно слышать, что посгрес в докере это оверхед.

Ну может кто-то 300 докеров запускает на одноплатниках хз ...

Думаю, больше чем 2 минуты, с учетом того что автоматический деплой придётся чуток усложнить. Плюс поддержка в долгосрочной переспективе. Плюс дампы и бэкапы..

Но не суть - даже если для этого достаточно бороду почесать, в чем профит то?

Мой опыт научил меня избавляться от любых излишних сложностей. Нам нужна база - какая? Postgres? Окей. А можем проще? Например, sqlite. Супер, берём её. А можем вообще от БД отказаться? Идеально! :D

Сложность проекта часто складывается не только из очень сложных компонентов и решений, но и из множества таких вот мелочей.

Ну в целом да. Но я всегда говорил, что выбор СУБД - это как выбор жены. Мир полон иллюзий сейчас, особенно из - за чистой архитектуры. Что можно легко поменять базу А на базу Б. А вот хрен там. База - это фундамент. На то она и база. Что умеет один фундамент, просто не умеет другой. Я бы выкинул на самом деле инверсию зависимостей из паттерна чистой в части работы с СУБД. Но это отдельная история

Спасибо за работу. Пишу пожелания - пока на запуске миграции.
1. Protos закинул в приватный пакет. Утомился разбираться, как забрать из приватного репозитория. Справился, кучу нового узнал.
2. Пакет Sqlite3 требует cgo(enabled+gcc). Потратил еще какую-то кучку времени. Нашел пакет https://gitlab.com/cznic/sqlite. Удалил пару троечек из /cmd/migator/main.go и заработало. Рекомендую внести изменение.

Добрался до интеграции с внешним сервисом

  • Много артефактов проекта в импорте: grpc-service-ref вместо sso - Ctrl+F по тексту!

  • В tests/suite/suite.go косячок: config.MustLoadPath(configPath)вместо config.MustLoad(). И здесь же неизвестный grpcHost, который или "", или напрашивается в конфиг.

  • Запуск go test ./tests -count=1 -v фейлится из-за пустого конфига. Не понял как аргумент пробросить сквозь go test, прописал путь в дефолт. Также не очевидно (но просто!), что тест нужно запускать параллельно с работающим app.

  • Неизвестный emptyAppID в функции TestLogin_FailCases. Предполагаю =0.

  • Если сразу несколько тестов, то паника "flag redefined". Не понял, как красиво сделать.


Sign up to leave a comment.

Articles