Перейти к основному содержимому

Spring Security — практический старт

Разработчику Архитектору

Spring Security — практический старт

После первого Spring Boot API возникает вопрос: кто может вызывать эндпоинты? Публичный URL без защиты подходит для учебы; в реальном API нужны логин, роли, токены.

Spring Security — стандартная библиотека в Java-экосистеме: цепочка фильтров проверяет запрос до вашего @RestController.


Словарь для старта

ТерминПростое объяснение
Аутентификация"Кто вы?" — проверка логина и пароля (или токена)
Авторизация"Что вам можно?" — доступ к /api/private, роль ADMIN
Фильтр (Filter)Код, который обрабатывает HTTP до контроллера; Security — набор таких фильтров
SecurityFilterChainУпорядоченный список правил: какой URL открыт, какой требует входа
HTTP BasicЛогин и пароль в заголовке Authorization: Basic ... (Base64); удобно для curl
401 Unauthorized"Вы не представились" — нет или неверные учётные данные
403 Forbidden"Вы вошли, но права не те" — роль или scope не подходят
CSRFАтака через подставной запрос из браузера; для stateless REST часто отключают осознанно
UserDetailsServiceИсточник пользователей для Spring (здесь — список в памяти)

В этой статье — минимальный, но рабочий сценарий:

  • GET /api/public — без авторизации;
  • GET /api/private — только с логином/паролем (HTTP Basic);
  • конфигурация через SecurityFilterChain (Spring Security 6+, Boot 3).

Обзор теории: Spring Framework · тесты API: JUnit 5.


Spring Security - цепочка фильтров вместо проверок в контроллере

Проверка if (apiKey == null) в каждом @GetMapping дублируется, легко забыть новый эндпоинт, сложно менять правила централизованно.

Spring Security держит правила в одном SecurityConfig: "эти пути открыты, остальное — только после входа". Контроллер занимается бизнес-логикой.


Как выглядит запрос с Basic-авторизацией

Клиент кодирует логин:пароль в Base64 и шлёт заголовок:

GET /api/private HTTP/1.1
Host: localhost:8080
Authorization: Basic ZGVtbzpzZWNyZXQ=

Разбор:

  • Строка GET /api/private HTTP/1.1 показывает, что клиент запрашивает защищённый ресурс методом чтения, и дальше срабатывает security-цепочка для этого URL.
  • Заголовок Host: localhost:8080 важен для корректной маршрутизации в локальном сервере и воспроизводимости примера в dev-окружении.
  • Authorization: Basic ZGVtbzpzZWNyZXQ= — это Base64 от demo:secret; фильтр BasicAuthenticationFilter декодирует его и передаёт данные в AuthenticationManager.
  • Если логин/пароль не совпадут, контроллер не вызывается, а ответ сразу формируется как 401 Unauthorized.
  • Именно поэтому в документации полезно показывать "сырой" HTTP: видно, какие данные реально приходят в фильтр до Java-кода контроллера.
curl -u demo:secret

Разбор:

  • Команда curl -u demo:secret автоматически формирует заголовок Authorization: Basic ..., поэтому вручную кодировать Base64 не нужно.
  • Параметр -u в данном случае сразу проверяет, что учётные данные реально принимаются сервером, а не только объявлены в конфигурации.
  • Этот вызов — минимальный smoke-check после изменения SecurityConfig: если он начал возвращать 401, значит сломалась цепочка аутентификации.
  • Важно выполнять его после старта приложения, иначе ошибку подключения можно принять за ошибку безопасности.
  • Практически это самый короткий способ подтвердить, что Basic Auth работает от клиента до фильтра.
curl -s http://localhost:8080/api/public
# {"message":"ok"}

curl -s http://localhost:8080/api/private
# 401 Unauthorized

curl -s -u demo:secret http://localhost:8080/api/private
# {"message":"hello, demo"}

Разбор:

  • Первая команда curl -s .../api/public подтверждает правило permitAll(): эндпоинт доступен без аутентификации и возвращает JSON {"message":"ok"}.
  • Вторая команда на .../api/private без -u ожидаемо даёт 401, то есть правило authenticated() сработало.
  • Третья команда с -u demo:secret демонстрирует успешный проход аутентификации и возврат защищённого ответа.
  • Ключевой смысл блока — показать не только "успех", но и два контрольных негативных/позитивных сценария доступа.
  • Такой набор удобно переносить в автопроверку, где эти три статуса становятся регрессионным тестом security-правил.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

Разбор:

  • Тег <dependency> объявляет библиотеку на уровне сборки, а не runtime-коде, поэтому без него SecurityFilterChain вообще не будет доступен в классах.
  • groupId + artifactId однозначно указывают Maven, какой starter подтянуть вместе с транзитивными зависимостями Spring Security.
  • Конкретно spring-boot-starter-security добавляет фильтры, auto-config и тестовые утилиты security-слоя.
  • После этой строки проект автоматически начинает требовать аутентификацию "по умолчанию", пока вы не настроите свои правила.
  • Практический вывод: это точка входа всей темы безопасности, и ошибка в ней делает бессмысленными последующие примеры кода.

Код ITЗагрузка примера кода…

Разбор:

  • Метод securityFilterChain(HttpSecurity http) собирает все правила в одном месте: что открыто, что защищено и какая схема логина используется.
  • Связка .csrf(csrf -> csrf.disable()) в этом учебном REST отключает CSRF-защиту для сценариев без cookie-сессий; в stateful web-приложениях это решение нужно принимать отдельно.
  • Внутри .authorizeHttpRequests(...) путь /api/public и /actuator/health открыт через permitAll(), а anyRequest().authenticated() закрывает всё остальное.
  • .httpBasic(Customizer.withDefaults()) включает стандартный Basic-обработчик без ручной сборки фильтров.
  • Второй бин userDetailsService() создаёт in-memory пользователя demo с ролью USER; префикс {noop} показывает, что пароль хранится без хеша только для обучения.
  • Возврат http.build() фиксирует итоговую цепочку фильтров; без этого объект правил не попадёт в runtime.

Код ITЗагрузка примера кода…

Разбор:

  • @RestController делает оба метода HTTP-обработчиками и сразу сериализует Map в JSON без шаблонов представления.
  • @GetMapping("/api/public") демонстрирует полностью открытый маршрут: метод возвращает статический {"message":"ok"} для быстрой проверки доступности.
  • @GetMapping("/api/private") — защищённый маршрут; сюда запрос попадёт только после успешной аутентификации в фильтрах.
  • Параметр @AuthenticationPrincipal UserDetails user показывает, как брать имя текущего пользователя из security-контекста, не парся заголовок вручную.
  • user.getUsername() подставляет в ответ фактического principal, поэтому удобно видеть, под кем выполнен вызов.
  • Разделение этих двух методов в одном классе хорошо иллюстрирует границу между публичной и приватной зоной API.
HTTP → SecurityFilterChain → (аутентификация?) → DispatcherServlet
→ HandlerMapping → preHandle() → @RestController → postHandle() → JSON → afterCompletion()

Разбор:

  • Схема показывает реальный порядок обработки: сначала HTTP-запрос попадает в security-фильтры, и только потом (если разрешено) доходит до DispatcherServlet.
  • Узел (аутентификация?) подчёркивает ключевой разветвитель: при ошибке логина цепочка завершается ответом 401/403 без вызова контроллера.
  • После диспетчера идут HandlerMapping, перехватчики и контроллер — см. жизненный цикл запроса в Spring Boot.
  • Переход к @RestController означает, что запрос прошёл авторизацию и теперь отрабатывает бизнес-логика endpoint-а.
  • Это полезный ментальный каркас для отладки: по месту сбоя понятно, искать проблему в конфиге безопасности или в коде контроллера.
  • В production ту же цепочку обычно дополняют логами корреляции и метриками по отказам на уровне фильтров.

Код ITЗагрузка примера кода…

Разбор:

  • @SpringBootTest поднимает полноценный контекст приложения, а @AutoConfigureMockMvc даёт MockMvc для HTTP-тестов без реального сетевого порта.
  • Тест publicOk() проверяет, что публичный маршрут действительно открыт и отдаёт 200 OK.
  • privateUnauthorized() фиксирует негативный сценарий: приватный endpoint без пользователя должен вернуть 401.
  • @WithMockUser(username = "demo") в privateWithUser() имитирует аутентифицированного пользователя и проверяет, что тот же endpoint уже возвращает 200.
  • Три теста вместе закрывают главный контракт раздела: публичный доступ, блокировка без входа и разрешение после аутентификации.
  • Такой набор регрессионных тестов защищает конфиг от случайного ослабления или ужесточения правил при рефакторинге.
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated())

Разбор:

  • Эта цепочка строит матрицу доступа на уровне URL: публичные маршруты, пользовательские и административные зоны.
  • requestMatchers("/api/public/**").permitAll() оставляет открытыми технические и общедоступные endpoint-ы.
  • hasRole("ADMIN") ограничивает административные операции только пользователями с ролью ROLE_ADMIN.
  • hasAnyRole("USER", "ADMIN") задаёт общий пользовательский уровень, куда допускаются оба типа аккаунтов.
  • anyRequest().authenticated() в конце закрывает все неописанные маршруты и исключает "случайно открытые" endpoint-ы.
  • Такая форма удобна для ревью: по одному блоку видно полный policy, который потом подтверждается тестами с @WithMockUser.
Дальше — production

Учебный пример с отключённым CSRF и паролем {noop} подходит для локального REST. Перед выкладкой в prod пройдите чеклист — HTTPS, CSP, хеширование паролей, секреты, SCA и OIDC — Spring Boot — безопасность в продакшене.