JWT и OAuth2 Resource Server в Spring Boot
JWT и OAuth2 Resource Server в Spring Boot
После HTTP Basic клиенту неудобно слать логин и пароль в каждом запросе. Схема JWT: один раз POST /auth/login → сервер отдаёт токен → дальше только Authorization: Bearer eyJhbG....
Stateless значит: сервер не хранит сессию в памяти; вся информация о пользователе и сроке жизни — внутри подписанного токена. Микросервисы масштабируются проще: любой инстанс проверяет подпись тем же секретом или публичным ключом.
Здесь — учебный монолит: свой эндпоинт выдачи токена + OAuth2 Resource Server для проверки. В проде токены обычно выдаёт Keycloak, Auth0 или корпоративный IdP — см. Spring Framework § Security и чеклист безопасности в продакшене.
Перед стартом: Spring Boot — первая программа, Spring Security — старт.
Словарь
| Термин | Объяснение |
|---|---|
| JWT (JSON Web Token) | Строка из трёх частей в Base64: заголовок, полезная нагрузка (claims), подпись |
| Claim | Поле внутри JWT: sub (кто), exp (когда истекает), scope, roles |
| Bearer | "Предъявитель токена" — тип авторизации в заголовке Authorization: Bearer ... |
| Resource Server | Ваш API, который принимает и проверяет токены (в отличие от сервера, который их выдаёт) |
| JwtEncoder / JwtDecoder | Подписать токен при логине / проверить подпись и срок при запросе |
| HS256 | Симметричный алгоритм: один секрет и для подписи, и для проверки (учебный вариант) |
| RS256 | Асимметричный: приватный ключ подписывает, публичный проверяет (типично в проде + JWKS) |
Сначала запустите приложение и получите токен через POST /auth/login, затем вызовите /api/me с заголовком Authorization: Bearer .... Только после этого разбирайте JwtEncoder и JwtDecoder.
Структура JWT (что внутри строки)
Токен выглядит как xxxxx.yyyyy.zzzzz (три блока через точку):
- Header — алгоритм (
HS256) и типJWT. - Payload — JSON с claims (
sub,exp, …); не шифруется, только подписывается — не кладите в JWT пароли и персональные данные без необходимости. - Signature — HMAC от header+payload секретом; подмена payload ломает подпись → 401.
Проверка на сервере — декодировать payload, убедиться что exp в будущем, пересчитать подпись тем же секретом.
Basic и JWT (когда что)
| HTTP Basic | JWT (Bearer) |
|---|---|
| Логин/пароль в каждом запросе (Base64 в заголовке) | Токен после /auth/login |
Проще для curl и внутренних скриптов | Удобно мобильным приложениям и SPA |
| Сессия на сервере опциональна | Обычно без серверной сессии (stateless) |
| Пароль "светится" чаще | Пароль один раз на /login; дальше только токен с ограниченным сроком |
Что получится
# 1) Получить токен
curl -s -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"demo","password":"secret"}'
# 2) Вызвать API с токеном (подставьте access_token из ответа)
curl -s http://localhost:8080/api/me \
-H "Authorization: Bearer eyJhbG..."
Разбор:
- Здесь показан воспроизводимый сценарий команд
curl`, `-H`, `-dдля проверки темы раздела на реальном запуске. - Команды идут как цепочка шагов: подготовка окружения, вызов инструмента и проверка ответа/результата.
- Такой блок полезен тем, что его можно выполнить без IDE и быстро подтвердить, что пример живой.
- Если результат отличается от ожидаемого, причина почти всегда в окружении или порядке выполнения строк.
- Практически этот же набор команд переносится в CI как smoke-проверка документационного примера.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Разбор:
- XML-блок через теги
dependency`, `groupId`, `artifactIdописывает конфигурацию декларативно, без ручной Java-инициализации. - Смысл фрагмента в том, что сборщик/контейнер читает структуру файла и сам подтягивает нужные зависимости.
- Ошибка в одном теге или координате сразу ломает весь сценарий, поэтому это критическая часть примера.
- Такой подход отделяет код приложения от инфраструктурных настроек и упрощает сопровождение.
app:
jwt:
# минимум 32 байта для HS256
secret: "change-me-change-me-change-me-32b!"
Разбор:
- Ключи
app`, `jwt`, `secretзадают runtime-поведение через конфигурацию, а не через изменения в коде. - Отступы формируют иерархию параметров, поэтому структура файла влияет на то, какие настройки реально применятся.
- Фрагмент полезен как точка контроля окружения — порт, логирование, имя сервиса или security-параметры.
- На практике такие блоки связывают с профилями и переменными окружения для dev/test/prod.
Код ITЗагрузка примера кода…
Разбор:
- В этом фрагменте ключевой объект —
JwtConfig; через него показан конкретный кусок архитектуры из раздела. - Аннотации
@Configuration, @Bean, @Valueвключают фреймворковое поведение — регистрация, биндинг, security или тестовая инфраструктура. - Основной поток строится на вызовах
secretKey()`, `Value()`, `getBytes()`, `SecretKeySpec(): они задают путь от входа к результату. - По
returnвидно, какой итог выходит наружу: объект домена, HTTP-ответ или конфигурационный результат. - При расширении этого примера сначала проверяют контракты методов и тесты на граничные случаи — именно там чаще всего возникает регрессия.
Код ITЗагрузка примера кода…
Разбор:
- В этом фрагменте ключевой объект —
SecurityConfig; через него показан конкретный кусок архитектуры из раздела. - Аннотации
@Configuration, @EnableWebSecurity, @Beanвключают фреймворковое поведение — регистрация, биндинг, security или тестовая инфраструктура. - Основной поток строится на вызовах
filterChain()`, `csrf()`, `disable()`, `authorizeHttpRequests(): они задают путь от входа к результату. - По
returnвидно, какой итог выходит наружу: объект домена, HTTP-ответ или конфигурационный результат. - При расширении этого примера сначала проверяют контракты методов и тесты на граничные случаи — именно там чаще всего возникает регрессия.
Код ITЗагрузка примера кода…
Разбор:
- В этом фрагменте ключевой объект —
LoginRequest, плюсTokenResponse; через него показан конкретный кусок архитектуры из раздела. - Аннотации
@RestController, @RequestMapping, @PostMapping, @RequestBodyвключают фреймворковое поведение — регистрация, биндинг, security или тестовая инфраструктура. - Основной поток строится на вызовах
LoginRequest()`, `TokenResponse()`, `RequestMapping()`, `AuthController(): они задают путь от входа к результату. - Условия
ifв этом блоке явно отделяют валидный и невалидный сценарий, чтобы ошибка отрабатывала до выполнения основной операции. - По
returnвидно, какой итог выходит наружу: объект домена, HTTP-ответ или конфигурационный результат. - При расширении этого примера сначала проверяют контракты методов и тесты на граничные случаи — именно там чаще всего возникает регрессия.
@RestController
public class MeController {
@GetMapping("/api/me")
public Map<String, Object> me(@AuthenticationPrincipal Jwt jwt) {
return Map.of(
"subject", jwt.getSubject(),
"expiresAt", jwt.getExpiresAt().toString()
);
}
}
Разбор:
- В этом фрагменте ключевой объект —
MeController; через него показан конкретный кусок архитектуры из раздела. - Аннотации
@RestController, @GetMapping, @AuthenticationPrincipalвключают фреймворковое поведение — регистрация, биндинг, security или тестовая инфраструктура. - Основной поток строится на вызовах
GetMapping()`, `me()`, `of()`, `getSubject(): они задают путь от входа к результату. - По
returnвидно, какой итог выходит наружу: объект домена, HTTP-ответ или конфигурационный результат. - При расширении этого примера сначала проверяют контракты методов и тесты на граничные случаи — именно там чаще всего возникает регрессия.
POST /auth/login → JwtEncoder → access_token
GET /api/me + Bearer → JwtAuthenticationFilter → JwtDecoder → контроллер
Разбор:
- Этот блок работает как схема: строка
POST /auth/login → JwtEncoder → access_tokenзадаёт каркас потока без деталей реализации. - Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.