Когда ты строишь API, первый вопрос, который встаёт после «как это всё вообще работает» — это «а кто вообще имеет право сюда ходить». Аутентификация — это не про безопасность ради безопасности, это про базовую гигиену. Без неё любой желающий дёрнет твой endpoint, и хорошо, если просто почитает данные, а не удалит или украдёт.
Есть три подхода, которые используются чаще всего: API-ключи, JWT и OAuth2. Они разные по сложности, по задачам и по тому, где ломаются. Разберём каждый нормально.
API-ключи — просто, грубо, но работает
API-ключ — это строка вроде sk-a3f9b2c1d4e5f678. Ты её генеришь, отдаёшь клиенту, он прикладывает к каждому запросу. Всё.
Обычно ключ передаётся в заголовке:
Authorization: Bearer sk-a3f9b2c1d4e5f678
Или в параметре запроса — но это хуже, потому что ключ попадёт в логи:
GET /api/data?api_key=sk-a3f9b2c1d4e5f678
Где это работает хорошо: server-to-server интеграции, когда у тебя есть сервис, который дёргает другой сервис, и оба под твоим контролем. OpenAI так устроен, Stripe так устроен, большинство SaaS-платформ — тоже.
Где ломается: если ключ утёк — всё, нужно его инвалидировать и выдать новый. Ключ не несёт никакой информации о пользователе — это просто пропуск. Если ты хочешь знать, кто именно к тебе пришёл, придётся делать lookup в базе при каждом запросе.
Ещё один минус — ротация. Если у тебя 50 клиентов с ключами, и один ключ скомпрометирован — ты не знаешь, кто с ним ходил, пока не поднял логи. Это больно.
Когда брать: внутренние интеграции, инструменты для разработчиков, B2B-API с небольшим числом клиентов.
JWT — токен, который сам о себе всё знает
JWT (JSON Web Token) — это не просто строка-пропуск, это токен с данными внутри. Выглядит страшно:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTYiLCJuYW1lIjoiSXZhbm92IiwiaWF0IjoxNzEzODI0MDAwLCJleHAiOjE3MTM4Mjc2MDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Но на самом деле это три части через точку: заголовок, payload и подпись. Payload — это JSON с данными, который можно декодировать (это не шифрование, а кодирование base64url). Там лежит что-то вроде:
{
"sub": "123456",
"name": "Иванов",
"role": "admin",
"iat": 1713824000,
"exp": 1713827600
}
Подпись сервер проверяет с помощью секретного ключа. Если кто-то попытается изменить payload — подпись не совпадёт, запрос отклонится.
Главное преимущество JWT: сервер не хранит сессии. Токен пришёл — сервер его проверил подписью, достал данные из payload, пошёл дальше. Никаких обращений к базе. Это делает JWT отличным выбором для горизонтально масштабируемых систем — не важно, на какой инстанс попадёт запрос, токен проверят везде одинаково.
Подводные камни JWT
Первый — токен нельзя «отозвать» до истечения срока. Если пользователь поменял пароль или тебе нужно срочно выбить сессию — с обычным JWT ты ничего не сделаешь до exp. Решение: либо хранить blacklist токенов (и тогда ты снова ходишь в базу), либо делать очень короткие сроки жизни — 15 минут, час.
Второй — алгоритм none. Это баг в реализациях, который позволял атакующему указать в заголовке "alg": "none" и сервер принимал токен без проверки подписи. Сейчас нормальные библиотеки это запрещают, но если ты пишешь свою реализацию — не делай так.
Третий — размер. JWT может вырасти, если ты запихнёшь в payload слишком много данных. Он передаётся с каждым запросом, и это начинает ощутимо давить на трафик.
Схема с refresh-токенами: access-токен живёт 15 минут, refresh-токен — 30 дней и хранится в httpOnly cookie. Когда access протух — клиент тихо идёт за новым с refresh. Это стандарт для большинства веб-приложений.
Когда брать: веб-приложения, мобильные приложения, микросервисы, где сервисы не хотят ходить к центральному хранилищу сессий.
OAuth2 — когда нужно делегировать доступ
OAuth2 — это не просто способ аутентификации, это протокол делегирования доступа. Та кнопка «Войти через Google» или «Войти через VK» — это OAuth2.
Суть: пользователь разрешает твоему приложению делать что-то от его имени в другом сервисе. Не отдаёт пароль — а именно делегирует конкретный набор прав (scope).
Вот упрощённый flow Authorization Code (самый распространённый):
- Пользователь нажимает «Войти через Google»
- Твоё приложение редиректит его на Google с параметрами:
client_id, redirect_uri, scope=email profile, state=случайная_строка
- Пользователь логинится в Google и разрешает доступ
- Google редиректит обратно с
code
- Твой бэкенд меняет
code на access_token и refresh_token (server-to-server, без пользователя)
- С
access_token идёшь в Google API за данными
Параметр state — это защита от CSRF. Ты генеришь случайную строку, кладёшь в сессию, отправляешь в запросе. Когда Google вернул code — проверяешь, что state совпадает. Без этого атакующий может подсунуть чужой code.
Разные grant types
Authorization Code — для веб и мобильных приложений с бэкендом. Самый безопасный.
Authorization Code + PKCE — для мобильных и SPA, где нет бэкенда для обмена кода. PKCE (Proof Key for Code Exchange) добавляет code_verifier и code_challenge, чтобы перехватить code было бесполезно.
Client Credentials — для server-to-server без пользователя. Сервис A говорит: «Я — сервис A, вот мой client_id и client_secret, дай токен». Это OAuth2-аналог API-ключей, но с возможностью управлять scope.
Resource Owner Password — пользователь отдаёт логин/пароль напрямую твоему приложению, оно само получает токен. Считается устаревшим и опасным — не используй, если не вынуждают.
OpenID Connect поверх OAuth2
OAuth2 говорит «можно ли делать X», но не говорит «кто ты». OpenID Connect (OIDC) добавляет id_token — это JWT с данными о пользователе. Это то, что используется, когда тебе нужно «войти через Google» и узнать email пользователя.
Когда брать OAuth2: когда нужна социальная авторизация, когда строишь платформу с третьими разработчиками, когда нужно гранулированное управление доступом с scope.
Сравниваем всё вместе
|
API-ключи |
JWT |
OAuth2 |
| Сложность реализации |
Низкая |
Средняя |
Высокая |
| Stateless |
Нет (lookup) |
Да |
Частично |
| Делегирование |
Нет |
Нет |
Да |
| Отзыв токена |
Легко |
Сложно |
Через refresh |
| Подходит для |
Server-to-server |
Веб/мобайл |
Третьи сервисы |
На практике эти подходы часто комбинируют. Например: OAuth2 для аутентификации пользователей, JWT как формат access-токена, а API-ключи для внутренних сервисов.
Что точно не делать
Не передавай токены в URL. GET-параметры попадают в логи сервера, в историю браузера, в реферер при переходе на другой сайт. Используй заголовок Authorization.
Не храни токены в localStorage. Любой XSS скрипт прочитает его. Для access-токенов с коротким сроком жизни — можно в памяти, для refresh — httpOnly cookie.
Не подписывай JWT симметричным ключом, если несколько сервисов проверяют токен. Если сервис может проверять токен, он может и выпускать. Используй RS256 (асимметричную подпись): приватный ключ только у сервиса аутентификации, публичный — у всех.
Не делай длинные access-токены. 15 минут — нормально. Час — максимум. Refresh-токены ротируй: при каждом использовании выдавай новый, старый инвалидируй.
Не игнорируй exp и nbf. exp — токен истёк, nbf (not before) — токен ещё не активен. Проверяй оба.
Практический пример: что выбрать для конкретного проекта
Допустим, ты строишь мобильное приложение с бэкендом:
- Авторизация пользователей: OAuth2 + OIDC (или просто JWT с username/password, если не нужна социальная авторизация)
- Токены: access JWT 15 минут + refresh 30 дней в httpOnly cookie
- Внутренние сервисы (например, сервис уведомлений вызывает API): Client Credentials или API-ключи
Допустим, ты строишь публичный API для разработчиков:
- Авторизация разработчиков: API-ключи с привязкой к аккаунту
- Если им нужно действовать от имени их пользователей: OAuth2 Authorization Code + PKCE
Допустим, ты строишь микросервисы:
- Service mesh с mTLS или JWT с RS256, где identity provider выдаёт токены сервисам
- Client Credentials для сервисных аккаунтов
Когда мы в REEXY проектируем API для клиентских проектов — часто начинаем с простого JWT, а OAuth2 добавляем тогда, когда реально нужна интеграция с третьими сервисами или делегирование. Усложнять сразу смысла нет.
Библиотеки, которые можно брать
Для JWT:
- Node.js:
jsonwebtoken, jose
- Python:
python-jose, PyJWT
- Go:
golang-jwt/jwt
- Java:
io.jsonwebtoken
Для OAuth2 (клиентская часть):
- Node.js:
passport, openid-client
- Python:
authlib
- Go:
golang.org/x/oauth2
Для OAuth2 (сервер авторизации):
- Keycloak — тяжёлый, но мощный
- Auth0, Okta — SaaS, быстро, но деньги
- Ory Hydra — open source, Go
- Authentik — self-hosted, хорошая альтернатива Keycloak
Итого
Выбор метода аутентификации зависит не от моды, а от задачи.
API-ключи — когда клиентов мало, доступ статичный, всё под твоим контролем.
JWT — когда нужен stateless, масштабируемый бэкенд с короткоживущими токенами.
OAuth2 — когда нужно делегирование, социальная авторизация или открытая платформа для сторонних разработчиков.
Чаще всего в реальном проекте всё три сосуществуют — и это нормально. Главное — понимать, зачем каждый из них здесь.