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