Содержание

Аутентификация, JWT и хранение паролей

Почти любой backend рано или поздно отвечает на два вопроса: кто делает запрос и имеет ли он право это делать. Первый вопрос называется аутентификацией. Второй - авторизацией.

Эти слова похожи, но путать их опасно:

  • аутентификация: "кто ты?";
  • авторизация: "что тебе можно?".

Пользователь может успешно войти в систему, но не иметь права удалить чужой ресурс. Значит, аутентификация прошла, а авторизация должна вернуть запрет.

Сценарий входа

Типичный сценарий входа:

  1. Пользователь отправляет email и пароль.
  2. Сервер находит пользователя по email.
  3. Сервер сравнивает пароль с хешем.
  4. Если все верно, сервер выдает access token.
  5. Клиент отправляет token в следующих запросах.

Важная деталь: сервер не должен хранить пароль в открытом виде. Он хранит только хеш.

Хранение пароля

Пароль хранится через специальную password hashing функцию, например bcrypt:

hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return fmt.Errorf("hash password: %w", err) }

В базу сохраняется hash, а не исходный пароль.

Проверка:

err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)) if err != nil { return ErrInvalidCredentials }

Почему нельзя использовать обычный SHA-256? Потому что пароли часто слабые, а атакующий может быстро перебирать миллионы вариантов. bcrypt специально медленнее и использует salt, чтобы усложнить массовый перебор.

Сообщения при ошибке входа

Не возвращайте разные ошибки:

email not found wrong password

Так атакующий может проверять, какие email зарегистрированы. Лучше единое сообщение:

invalid credentials

В логах можно сохранить техническую причину аккуратно, без пароля.

JWT

JWT состоит из трех частей:

header.payload.signature
  • header описывает алгоритм;
  • payload содержит claims;
  • signature доказывает, что token подписан сервером.

Payload можно прочитать без секрета. Поэтому JWT - это подписанный, а не зашифрованный токен.

Хорошие claims:

{ "sub": "user_id", "exp": 1760000000, "iat": 1759990000, "role": "student" }

Плохие claims:

{ "password": "...", "api_key": "...", "credit_card": "..." }

exp, iat и sub

Частые claims:

  • sub - subject, обычно id пользователя;
  • exp - когда token истекает;
  • iat - когда token был выпущен;
  • iss - кто выпустил token;
  • aud - для кого token предназначен.

Минимально полезный JWT почти всегда должен иметь sub и exp. Token без срока действия сложно отозвать и опасно хранить.

Access и refresh tokens

Access token живет недолго: например, 15-60 минут. Его отправляют в API-запросах:

Authorization: Bearer <access_token>

Refresh token живет дольше и используется для получения нового access token.

Почему не сделать один вечный token? Потому что token может утечь. Короткий access token уменьшает окно риска. Refresh token можно хранить более осторожно, ротировать и отзывать.

В реальных системах refresh token часто хранят серверно или делают отзывным, чтобы пользователь мог выйти из аккаунта на конкретном устройстве.

Middleware авторизации

Middleware читает заголовок:

Authorization: Bearer <token>

Затем:

  1. проверяет формат;
  2. проверяет подпись;
  3. проверяет срок действия;
  4. достает user_id;
  5. кладет данные пользователя в context запроса.
func AuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token := extractBearerToken(r.Header.Get("Authorization")) if token == "" { http.Error(w, "unauthorized", http.StatusUnauthorized) return } userID, err := verifyToken(token) if err != nil { http.Error(w, "unauthorized", http.StatusUnauthorized) return } ctx := context.WithValue(r.Context(), userIDKey{}, userID) next.ServeHTTP(w, r.WithContext(ctx)) }) }

В handler после middleware уже можно получить текущего пользователя из context.

401 и 403

401 Unauthorized означает: пользователь не аутентифицирован. Например, token отсутствует или неверный.

403 Forbidden означает: пользователь известен, но права недостаточны.

if review.UserID != currentUser.ID && currentUser.Role != "admin" { http.Error(w, "forbidden", http.StatusForbidden) return }

Frontend и API-клиенты по-разному реагируют на эти статусы. При 401 можно отправить пользователя на login. При 403 нужно показать, что доступа нет.

Роли и права

Простой вариант авторизации - роли:

  • student;
  • mentor;
  • admin.

Но роль не всегда достаточно точна. Иногда нужна проверка владения ресурсом:

if book.OwnerID != currentUser.ID { return ErrForbidden }

Хороший backend не доверяет frontend. Даже если кнопка удаления скрыта в UI, сервер все равно должен проверить право на удаление.

Где хранить token

В web-приложениях часто выбирают между:

  • memory;
  • localStorage;
  • httpOnly cookie.

У каждого варианта есть trade-off. localStorage удобен, но уязвимее при XSS. httpOnly cookie недоступна JavaScript, но требует аккуратной настройки SameSite, Secure и CSRF-защиты для некоторых сценариев.

Для учебного backend важно запомнить базу: token - это credential. Относитесь к нему как к паролю.

Чеклист

  1. Пароли хранятся через bcrypt или современную password hashing функцию.
  2. Исходный пароль не логируется и не сохраняется.
  3. Ошибки login не раскрывают, существует ли email.
  4. JWT не содержит секретов.
  5. У token есть exp.
  6. Access token живет недолго.
  7. Refresh token можно отозвать или ротировать.
  8. Middleware отделена от обработчиков.
  9. 401 и 403 используются по смыслу.
  10. Авторизация проверяется на сервере, не только во frontend.

Следующий шаг после статьи

Закрепите тему в реальном проекте и продолжайте обучение на курсах Praxis.

Продолжить изучение

Выбери следующую статью по маршруту или углубись в смежную тему.

Похожие статьи