JavaRush /Курсы /Spring Security /oauth2ResourceServer ...

oauth2ResourceServer ( ) . jwt ( ) : что внутри

Spring Security
25 уровень , 0 лекция
Открыта

1. Второй путь поверх custom JWT filter

Когда вы впервые собрали JWT-аутентификацию через свой OncePerRequestFilter, ощущение обычно такое: «ура, я победил монстра». И это честное ощущение — вы действительно собрали stateless-аутентификацию своими руками и увидели, как запрос превращается в “current user” внутри SecurityContext. Но тут появляется взрослый вопрос: если мы написали инфраструктурный код сами, готовы ли мы его поддерживать так же, как Spring Security поддерживает свой?

Custom filter — отличный учебный инструмент, потому что он максимально прозрачен: вы буквально видите каждую строчку, где извлекается header, где валидируется подпись, где создаётся Authentication. Но в реальном проекте подобный код становится зоной риска. Ошибка в обработке “пустого токена”, лишний лог с токеном целиком, неправильный порядок фильтров, случайно проглоченное исключение — и вот ваш API внезапно живёт в странном мире 401/403, где «вроде всё правильно, но клиент всё равно плачет».

Встроенная поддержка Spring Security — то, что мы включаем через oauth2ResourceServer().jwt() — это не новая модель безопасности и не «другая философия JWT». Это тот же stateless Bearer token подход, но собранный framework-native способом. То есть мы не меняем смысл, мы меняем “кто держит в руках отвёртку”.

Чтобы не смешать два механизма в одном запущенном приложении, удобно держать built-in путь в отдельном jwt-rs профиле того же API. Custom-filter вариант остаётся точкой сравнения, но в активной цепочке Bearer-механизм нужен ровно один.

2. Главное разделение: выдача токена и проверка токена

Самая частая путаница у новичков начинается здесь: видим oauth2... видим resourceServer, и мозг такой: «ага, значит это про логин, про регистрацию, про “дай токен”». На самом деле всё наоборот. Выдача JWT — это наш прикладной flow (наш endpoint POST /api/auth/login). А oauth2ResourceServer().jwt() — это про проверку входящего Bearer JWT на каждом защищённом запросе.

Чтобы это не оставалось красивыми словами, полезно держать в голове очень простую картину: один endpoint создаёт токен, а все остальные защищённые endpoints принимают токен и решают, можно ли доверять запросу.

Небольшая схема (обратите внимание: она про бизнес-уровень, а не про внутренности цепочки фильтров):

flowchart LR
    C[Клиент] -->|"POST /api/auth/login (username+password)"| API["Secure Content Platform API"]
    API -->|"200 + JWT"| C
    C -->|"GET /api/me Authorization: Bearer JWT"| API
    API -->|"200 + JSON"| C

И вот здесь кроется ключевой смысл: oauth2ResourceServer().jwt() не заменяет POST /api/auth/login. Оно делает так, чтобы запросы после логина проверялись корректно, без вашего самописного фильтра.

Чтобы связь с проектом была прямой, вот минимальный “кусочек” контроллера логина (он у нас уже существует и не должен исчезнуть после включения built-in path):

import org.springframework.web.bind.annotation.*;

@RestController
public class AuthController {

    private final AuthService authService;

    // AuthService — ваш прикладной сервис: проверка логина/пароля и выдача JWT
    public AuthController(AuthService authService) {
        this.authService = authService;
    }

    @PostMapping("/api/auth/login")
    public AuthResponse login(@RequestBody LoginRequest request) {
        // Важно: этот endpoint выдаёт токен (built-in resource server этого не делает)
        return authService.login(request);
    }
}

Смысл фрагмента прост: endpoint остаётся вашим. Spring Security тут не “магически логинит” — он лишь даёт вам AuthenticationManager, провайдеры и общую архитектуру. А выдача токена — это решение приложения.

3. Resource server mode в рамках курса

Фраза “resource server” звучит так, будто мы сейчас уезжаем в большой OAuth2-мир, где есть внешний сервер авторизации, consent screen, коды, редиректы и прочие приключения. В этом курсе мы туда не едем. Мы используем часть механики, которая решает очень конкретную задачу: принять Bearer JWT, проверить его и восстановить current user на время запроса.

Если объяснить “resource server mode” человечески, то получится почти бытовая аналогия. Представьте клуб с охраной. На входе у человека есть пропуск (JWT). Охранник не выдаёт пропуск — он только проверяет, что он настоящий, не просроченный и не подделанный. Если всё хорошо, охранник ставит на руку штамп “ок” (это и есть появление аутентификации в SecurityContext).

Слово oauth2 в DSL — это, по сути, “историческое имя раздела”, потому что JWT validation в Spring Security живёт в пакете resource server. Но наша реальность в проекте остаётся прежней: у нас своё API, свой login endpoint, свой TokenService, и мы хотим, чтобы запросы к /api/me, /api/drafts, /api/editor/** и /api/admin/** нормально проверяли Bearer token.

Это важно проговорить ещё и потому, что иначе студент начинает “переписывать архитектуру в голове” раньше времени: «раз уж oauth2ResourceServer, то надо выкинуть AuthenticationManager из логина» или «значит JWT теперь надо получать у кого-то извне». Не надо. Мы просто заменяем самописный “пограничный контроль” на встроенный.

4. Изменения в SecurityFilterChain

На уровне кода всё выглядит подозрительно просто — и это хороший знак. Мы не собираем третью security-модель и не включаем её рядом с custom filter. Мы берём тот же API, запускаем его в jwt-rs профиле и меняем только способ проверки Bearer JWT.

Минимальная строка (внутри вашего SecurityConfig) выглядит так:

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

// Включаем режим resource server: приложение принимает Bearer JWT на входе
http.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> {
    // Тут можно настраивать JwtDecoder / JwtAuthenticationConverter и т.п.
}));

Если собрать чуть более реалистичный фрагмент — с уже привычной нам stateless-настройкой — получится так:

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;

// JWT API почти всегда работает без сессий: каждый запрос несёт токен сам
http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
    // Включаем встроенную проверку Bearer JWT
    .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> {
        // Конвертация claims -> authorities обычно настраивается здесь
    }));

Обратите внимание на два момента.

Первый момент: stateless остаётся обязательным. То, что мы включили JWT-поддержку, не означает, что приложение внезапно возвращается к сессиям. Наоборот: resource server режим — это типичная история “каждый запрос несёт аутентификацию сам”.

Второй момент: oauth2ResourceServer().jwt() — это не “правила доступа”. Это не заменяет authorizeHttpRequests. Ваши правила по URL и методам, ваши hasRole(...), ваши hasAuthority(...), ваши authenticated() остаются на месте. То есть вы по-прежнему должны сказать: “/api/public/** — можно всем”, “/api/admin/** — только ADMIN”, “всё остальное — хотя бы аутентифицированным”.

Например (логика та же, что у нас уже была; важное здесь — увидеть, что оно не меняется):

http.authorizeHttpRequests(auth -> auth
    // Публичные маршруты: логин и контент без аутентификации
    .requestMatchers("/api/public/**", "/api/auth/**").permitAll()
    // Админские маршруты: только роль ADMIN
    .requestMatchers("/api/admin/**").hasRole("ADMIN")
    // Всё остальное: нужен валидный JWT (аутентификация)
    .anyRequest().authenticated()
);

Критический нюанс: один JWT‑механизм

Если в предыдущем checkpoint вы добавляли свой фильтр примерно так:

// было в custom JWT ветке (примерно)
// Важно: этот фильтр вручную доставал JWT и выставлял Authentication в SecurityContext
http.addFilterBefore(jwtAuthFilter, /* какой-то стандартный фильтр */);

то при включённом built-in path этот “самодельный КПП” нужно вывести из активной цепочки. Если custom JWT ветка остаётся в проекте для сравнения, держите её вне jwt-rs профиля. В одном запущенном приложении нужен ровно один Bearer/JWT механизм. Иначе вы почти гарантированно получите странности: где-то выставите SecurityContext, где-то перетрёте его, где-то дважды обработаете ошибку, и отладка превратится в сериал на 12 сезонов.

Принцип здесь простой: в одном активном профиле проекта — один Bearer/JWT механизм.

5. Зависимости Gradle для resource server

Когда вы пишете .oauth2ResourceServer(...).jwt(...), вы как бы говорите: “я хочу пользоваться готовыми компонентами проверки JWT”. Но эти компоненты не возникают из воздуха. В Spring Security они упакованы в отдельные модули, потому что не всем приложениям нужен resource server режим.

Поэтому в jwt-rs профиле проекта вам понадобятся зависимости для resource server и JOSE (то есть для JWT/JWS-части: подпись, алгоритмы и т.д.).

Минимальный фрагмент build.gradle.kts может выглядеть так:

dependencies {
    // Базовая инфраструктура Spring Security (фильтры, конфигурация, контракты)
    implementation("org.springframework.boot:spring-boot-starter-security")

    // Режим resource server: приём Bearer token и валидация JWT на входящих запросах
    implementation("org.springframework.security:spring-security-oauth2-resource-server")

    // JOSE: подписи/алгоритмы, работа с JWS/JWT (то, чем проверяется токен)
    implementation("org.springframework.security:spring-security-oauth2-jose")
}

И тут важно не перепутать: spring-boot-starter-security — это “общая дверь” в Spring Security (фильтры, конфигурация, базовые интерфейсы). А вот resource server + jose — это конкретно “умеем разбирать Bearer token и валидировать JWT”.

Если этих зависимостей нет, ваш DSL-вызов либо не скомпилируется, либо поведение будет неполным. То есть “одной красивой строчки” недостаточно — мы включаем реальную функциональность, и она должна быть в classpath.

6. Что не меняется в проекте

После включения built-in JWT support не нужно переписывать весь security-слой. Меняется только инфраструктура проверки токена на входе.

POST /api/auth/login остаётся вашим прикладным flow и по-прежнему выдаёт токен через TokenService. authorizeHttpRequests остаётся картой доступа: публичные маршруты, /api/me, editor/admin зоны описываются теми же правилами, что и раньше.

То же самое с method security и owner-based проверками. Сервису всё равно, появился Authentication из session, HTTP Basic, custom JWT filter или oauth2ResourceServer().jwt(). Для него важно одно: в SecurityContext есть пользователь и набор authorities, по которым уже решаются публикация, модерация и правила “свой / чужой”.

7. Типичные ошибки

Ошибки на этом этапе обычно не про “не знаю аннотацию”, а про неправильную модель в голове. Люди начинают ожидать от built-in JWT path того, чего он по смыслу делать не должен, или, наоборот, пытаются выкинуть куски системы, которые остаются обязательными. Ниже — самые частые грабли, на которые наступают почти все (это как LEGO, только больнее).

Ошибка №1: думать, что oauth2ResourceServer().jwt() “делает логин”.
Встроенная JWT-поддержка не заменяет POST /api/auth/login. Она не умеет аутентифицировать пользователя по username/password, не выдаёт токен и не знает вашу бизнес-логику. Её зона ответственности — запросы, которые уже пришли с Bearer token, и их проверка на входе.

Ошибка №2: забыть, что stateless‑режим остаётся обязательным решением.
Иногда включают built-in JWT path, но оставляют конфигурацию так, будто приложение живёт в session-based мире. В результате появляются странные побочные эффекты: где-то ожидается сессия, где-то нет, и SecurityContext ведёт себя “не как в лекциях”. Если вы строите JWT-only API, держите SessionCreationPolicy.STATELESS как явный и осознанный выбор.

Ошибка №3: ожидать, что access rules “заменятся сами собой”.
oauth2ResourceServer().jwt() — это не authorizeHttpRequests. Он не решает, кому можно в /api/admin/**. Он лишь создаёт аутентификацию (если токен валидный). А дальше уже ваши правила (hasRole(...), hasAuthority(...), @PreAuthorize) решают, что доступно этому пользователю.

Ошибка №4: оставить в цепочке и custom JWT filter, и built‑in путь одновременно.
Это одна из самых неприятных ошибок, потому что она не всегда “падает сразу”. Иногда всё стартует, но поведение становится непредсказуемым: двойные попытки аутентификации, разные форматы ошибок, неожиданные 403 там, где вы ждали 401. На одном наборе endpoint’ов выбирайте один подход. Если оба варианта лежат в одном репозитории, разводите их по разным профилям и запускайте только один за раз.

Ошибка №5: начать “чинить” бизнес‑авторизацию, когда проблема вообще не в ней.
Если после включения built-in JWT path вы видите неожиданный отказ, первый импульс — переписать @PreAuthorize или поменять hasAuthority на hasRole. Часто это преждевременно: прежде чем ломать бизнес-правила, убедитесь, что вы правильно разделяете “пользователь не аутентифицирован” и “пользователь аутентифицирован, но не имеет права”. Именно это разделение спасает проект от хаоса в 401/403 и помогает не лечить симптом вместо причины.

1
Задача
Spring Security, 25 уровень, 0 лекция
Недоступна
Встроенная Bearer/JWT-защита для простого API
Встроенная Bearer/JWT-защита для простого API
1
Задача
Spring Security, 25 уровень, 0 лекция
Недоступна
Отдельный login flow при built-in JWT path
Отдельный login flow при built-in JWT path
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ