Аутентификация и авторизация
Кто ты и что тебе разрешено — два разных вопроса, на которые API отвечает по-разному.
Аутентификация (authn) устанавливает, кто делает запрос; авторизация (authz) решает, что этому субъекту позволено.
Любой публичный API рано или поздно сталкивается с вопросом доступа: данные одного пользователя нельзя отдавать другому, платные функции — бесплатным аккаунтам, а административные операции — кому попало. Разделение на две стадии — сначала опознать, потом проверить права — лежит в основе всех схем безопасности REST. Перепутать их легко: например, проверить токен на валидность (authn пройдена), но забыть убедиться, что объект принадлежит именно этому пользователю (authz провалена). Это одна из самых дорогих ошибок в индустрии.
Authn против authz на конкретике
Представьте запрос на удаление чужого комментария. Authn отвечает на вопрос «это вообще зарегистрированный пользователь Иван?» — по токену видно, что да. Authz отвечает на вопрос «можно ли Ивану удалять этот комментарий?» — и если комментарий написал Пётр, ответ «нет», даже при идеально валидном токене. Authn возвращает 401 Unauthorized (личность не установлена), authz — 403 Forbidden (личность известна, но прав не хватает).
API-ключи: просто, но статично
Самый простой способ опознать клиента — выдать ему длинную случайную строку (ключ) и попросить присылать её с каждым запросом. Ключ обычно генерируется криптографически стойким случайным образом и хранится на стороне сервиса в хэшированном виде — так же, как пароли, чтобы утечка базы не раскрыла действующие ключи. Обычно ключ кладут в заголовок:
curl https://api.example.com/v1/orders \
-H "X-API-Key: sk_live_4eC39HqLyjWDarjtT1zdp7dc"
Плюсы: понятно, быстро интегрируется, удобно для server-to-server. Минусы существенны: ключ статичен — он не истекает сам, не содержит информации о пользователе и при утечке даёт полный доступ, пока его вручную не отзовут. Поэтому API-ключи годятся для бэкенд-интеграций, но плохо подходят для приложений в браузере, где их легко украсть.
JWT: самодостаточный токен
JSON Web Token решает проблему «токен ничего о себе не знает». Это строка из трёх частей, разделённых точками: header.payload.signature. Каждая часть — это Base64URL.
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMiLCJleHAiOjE3MDAwMDB9.s5d8F_signature_bytes
└─────── header ──────┘ └──────────── payload ───────────┘ └─── signature ───┘
В декодированном виде это выглядит так:
{ "alg": "HS256", "typ": "JWT" }
{ "sub": "123", "role": "user", "exp": 1700000000, "iat": 1699996400 }
- header — алгоритм подписи (
HS256,RS256) и тип токена. - payload — «claims»:
sub(кто),exp(когда истекает, Unix-время),role, любые свои поля. - signature — подпись от
header + payloadсекретным ключом сервера.
Ключевое свойство — stateless: сервер не хранит токен у себя. Получив JWT, он пересчитывает подпись и сравнивает; если совпало и exp ещё не наступил — токен подлинный. Не нужна база сессий, любой инстанс кластера проверит токен сам. Платой за это становится истечение: пока токен живёт, его нельзя «выключить» без дополнительных механизмов (короткий exp + refresh-токены, чёрные списки).
Bearer-токен в заголовке Authorization
Стандартный способ передать токен (JWT или OAuth) — заголовок Authorization со схемой Bearer («предъявитель»):
curl https://api.example.com/v1/me \
-H "Authorization: Bearer <token>"
Слово Bearer означает «кто предъявил — тот и владелец»: токен не привязан к устройству, поэтому украденный Bearer-токен работает у злоумышленника так же, как у вас. Отсюда два правила: только HTTPS и короткое время жизни.
OAuth2: обзор ролей и authorization code flow
OAuth2 нужен, когда стороннее приложение хочет действовать от вашего имени, не получая ваш пароль. Роли:
- Resource Owner — пользователь (владелец данных).
- Client — приложение, которому нужен доступ.
- Authorization Server — выдаёт токены (например, accounts.google.com).
- Resource Server — собственно API с данными.
Самый распространённый сценарий — authorization code flow:
Пользователь Client Auth Server Resource Server
| жму "Войти" | | |
|----------------->| редирект на login | |
|<--------- редирект -------------->| |
| логинюсь, разрешаю доступ | |
|<------ редирект с ?code=ABC ------| |
|----------------->| code=ABC | |
| |-- code + secret -->| |
| |<--- access_token --| |
| |------ Bearer access_token ------------>|
| |<------------- данные -------------------|
Суть: клиент сначала получает одноразовый code через браузер, затем меняет его на access_token в прямом серверном запросе со своим секретом. Так токен не светится в адресной строке. Полученный access_token обычно короткоживущий, и вместе с ним выдаётся refresh_token — долгоживущий, по которому клиент тихо получает новый access без повторного логина пользователя. Каждый токен ограничен набором прав через scopes (например, read:profile, но не write:payments), поэтому стороннее приложение получает ровно столько доступа, сколько пользователь явно одобрил — а не весь аккаунт целиком, как было бы при передаче пароля.
Как работает под капотом
Проверка JWT на сервере — это перевычисление подписи. Сервер берёт base64(header) + "." + base64(payload), подписывает своим ключом тем же алгоритмом и сравнивает с третьей частью токена. Совпало — данные не подделаны; не совпало — токен отвергается. Затем проверяется exp: если текущее время больше — 401. Важно: payload не зашифрован, а лишь подписан, поэтому в него нельзя класть секреты — любой может его раскодировать. Подпись защищает от изменения, но не от чтения.
Где хранить токен на клиенте
Два популярных варианта и их риски:
| Хранилище | Плюс | Риск |
localStorage | просто, доступно из JS | любой XSS читает токен напрямую |
| httpOnly cookie | JS не видит токен → XSS не украдёт | уязвима к CSRF без защиты |
Если на странице сработает XSS-инъекция (внедрённый скрипт), токен из localStorage утекает мгновенно. Cookie с флагом httpOnly недоступна скриптам, поэтому считается безопаснее против XSS, но требует защиты от CSRF (флаг SameSite, CSRF-токены). Общая рекомендация: для чувствительных приложений — httpOnly + Secure + SameSite.
Частые ошибки
- Считать, что валидный токен = есть права. Authn пройдена, а authz объекта забыта — классическая дыра (BOLA).
- Класть в JWT-payload пароли или приватные данные — payload читается кем угодно.
- Бессрочные токены без
exp— украденный токен работает вечно. - Хранить токен в
localStorageв приложении с пользовательским контентом, открытым для XSS. - Передавать токен по HTTP без TLS — Bearer перехватывается в открытом виде.
- Возвращать
403там, где личность вообще не установлена (нужен401), и наоборот.
Итоги
- Authn (кто ты) и authz (что можно) — разные стадии; коды
401и403соответственно. - API-ключи просты, но статичны и не истекают — для server-to-server.
- JWT самодостаточен (
header.payload.signature), stateless, но обязан истекать. - Токен передают в
Authorization: Bearer <token>; Bearer = «у кого токен, тот и владелец» → только HTTPS. - OAuth2 даёт доступ без пароля; authorization code flow меняет одноразовый code на токен.
- httpOnly cookie безопаснее
localStorageпротив XSS, но требует защиты от CSRF.