JWT-токены
JWT-токены: как API узнаёт пользователя без сессий на сервере.
Суть: JWT (JSON Web Token) — это подписанный токен, который сервер выдаёт при входе, а клиент потом присылает в каждом запросе. Сервер проверяет подпись и доверяет данным внутри — не храня сессию у себя. Это удобно для stateless-API.
В мире API популярна авторизация по токенам: после логина клиент получает JWT и кладёт его в заголовок Authorization: Bearer токен каждого запроса. Сервер по подписи понимает, что токен подлинный и не подделан.
Структура JWT
Токен состоит из трёх частей через точку: Header (алгоритм), Payload (claims: id, роль, срок) и Signature (подпись). Подпись считается секретным ключом сервера — подделать payload без ключа нельзя.
header.payload.signature
| | |
| | +-- подпись секретом сервера (нельзя подделать)
| +-- claims: { "sub": "42", "role": "Admin", "exp": ... }
+-- { "alg": "HS256", "typ": "JWT" }
Подключение в ASP.NET Core
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true, // срок действия
ValidateIssuerSigningKey = true,
ValidIssuer = jwt.Issuer,
IssuerSigningKey = new SymmetricSecurityKey(keyBytes)
};
});
Как работает под капотом
При логине сервер собирает claims, кодирует header и payload в Base64Url, считает подпись секретным ключом и склеивает токен. Клиент хранит его и шлёт в заголовке. На каждом запросе AddJwtBearer разбирает токен, пересчитывает подпись тем же ключом и сравнивает: совпало — токен подлинный; проверяет срок (exp), издателя, аудиторию. Если всё ок — claims из payload попадают в HttpContext.User. Сервер не хранит сессий: вся нужная информация — в самом токене. Проверим идею подписи на Python.
# Идея JWT-подписи: payload + секрет -> подпись, проверка пересчётом
import hashlib
SECRET = "super-secret-key"
def sign(payload):
return hashlib.sha256((payload + SECRET).encode()).hexdigest()
def make_token(payload):
return payload + "." + sign(payload)
def verify(token):
payload, signature = token.rsplit(".", 1)
return sign(payload) == signature # пересчитываем тем же секретом
tok = make_token('{"sub":"42","role":"Admin"}')
print("Токен:", tok)
print("Валиден:", verify(tok))
# Подделка payload без секрета -> подпись не сойдётся
forged = '{"sub":"42","role":"Hacker"}.' + tok.rsplit(".", 1)[1]
print("Подделка валидна:", verify(forged))
Попробуй сам ▶ — настоящий токен проходит проверку, а подделанный payload (Admin заменён на Hacker) ломает подпись. Ровно так JWT защищает от подмены.
Частые ошибки
- Хранить секретный ключ в коде/репозитории. Утечка ключа равна возможности выпускать любые токены. Ключ — в секретах.
- Не проверять срок (
exp). Бессрочные токены опасны; всегда включайтеValidateLifetime. - Класть в payload секреты. Payload не зашифрован, а лишь подписан — он читается любым, кто получит токен.
Best practices
- Делайте access-токены короткоживущими, обновляйте через refresh-токены.
- Передавайте JWT только по HTTPS — иначе токен можно перехватить.
- Храните ключ в надёжном месте (секреты окружения, vault), а не в appsettings.json.
Подпись против шифрования и срок жизни
Частое заблуждение — что JWT «зашифрован». На самом деле стандартный JWT подписан, но не зашифрован: header и payload закодированы Base64Url (это кодировка, а не шифр) и читаются кем угодно, у кого есть токен. Подпись лишь гарантирует целостность — что содержимое не подменили, ведь пересчитать корректную подпись без секретного ключа сервера нельзя. Отсюда железное правило: в payload кладут только то, что не страшно показать (id, роли, срок), но никогда — пароли и секреты.
Подпись бывает симметричной (HS256, один секрет и для выпуска, и для проверки) и асимметричной (RS256, приватный ключ выпускает, публичный проверяет — удобно, когда токен проверяют разные сервисы). В обоих случаях проверка сводится к пересчёту подписи и сравнению — ровно это смоделировала запускаемая врезка выше: подмена роли в payload ломает подпись, и токен отвергается.
Access, refresh и stateless-цена
Stateless-природа JWT — одновременно достоинство и сложность. Достоинство: сервер не хранит сессий, легко масштабируется горизонтально, любой инстанс проверит токен сам. Сложность: выданный токен нельзя «отозвать» простым удалением сессии — он валиден до истечения exp. Поэтому access-токены делают короткоживущими (минуты), а для продления используют refresh-токены — долгоживущие, хранимые на сервере и отзываемые. Клиент по истечении access-токена меняет refresh на новую пару. Так совмещают удобство stateless-проверки и контроль над доступом.
Безопасность JWT держится на нескольких правилах, и пренебречь нельзя ни одним. Секретный ключ хранят в секретах окружения, а не в коде и не в appsettings под git — утечка ключа означает, что атакующий выпустит любой токен с любой ролью. Всегда включают проверку срока (ValidateLifetime), издателя и аудитории. И передают токен исключительно по HTTPS: по открытому каналу его перехватят и используют как есть — никакая подпись от перехвата не спасает, ведь это валидный токен.
Итог: JWT переносит личность в подписанном токене, позволяя API быть stateless. Дальше — как навешивать защиту на эндпоинты и строить политики.