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

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=

curl -u demo:secret делает это автоматически. Сервер сравнивает с пользователем из UserDetailsService; при успехе запрос доходит до контроллера, при ошибке — 401 без вызова вашего метода.


Что получится

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"}

Зависимости

На start.spring.io добавьте Spring Web и Spring Security к существующему проекту из 271 или создайте новый.

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

Конфигурация безопасности

Класс SecurityConfigединственное место, где задаются правила (не размазывайте по контроллерам).

package com.example.demo.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // для учебного REST; в проде — осознанно
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public", "/actuator/health").permitAll()
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults());
return http.build();
}

@Bean
UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(
User.withUsername("demo")
.password("{noop}secret")
.roles("USER")
.build());
}
}

Разбор конфигурации по строкам:

ФрагментЧто делает
@Configuration + @EnableWebSecurityКласс с правилами безопасности включён в контекст Spring
csrf.disable()Отключает проверку CSRF-токена; для учебного REST без cookie-сессий это типично; для форм в браузере — включайте обратно
requestMatchers("/api/public", ...).permitAll()Эти URL доступны без логина
anyRequest().authenticated()Все остальные пути — только после успешной аутентификации
httpBasic(...)Включить схему Basic (заголовок Authorization)
InMemoryUserDetailsManagerПользователи хранятся в RAM; после перезапуска — те же логины
User.withUsername("demo").password("{noop}secret"){noop} = пароль без хеширования (только обучение)
.roles("USER")Роль с префиксом ROLE_USER внутри; в правилах пишут hasRole("USER")

BCrypt в проде: пароль хранят как хеш ($2a$10$...), в конфиге указывают PasswordEncoder bean. Spring сравнивает введённый пароль с хешем, открытый текст в БД не лежит.

:::caution Продакшен In-memory пользователи и {noop} — только для обучения. В реальных системах: БД/LDAP, BCrypt (passwordEncoder), JWT или OAuth2 Resource Server — см. 27.md. :::


Контроллер

package com.example.demo.web;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
public class ApiController {

@GetMapping("/api/public")
public Map<String, String> publicEndpoint() {
return Map.of("message", "ok");
}

@GetMapping("/api/private")
public Map<String, String> privateEndpoint(@AuthenticationPrincipal UserDetails user) {
return Map.of("message", "hello, " + user.getUsername());
}
}

@AuthenticationPrincipal UserDetails user — после успешного входа Spring кладёт объект пользователя в контекст безопасности; параметр метода заполняется автоматически. Так вы получаете user.getUsername() без разбора заголовка вручную.

Разбор эндпоинтов:

МетодURLКто вызываетРезультат
publicEndpoint/api/publicЛюбой{"message":"ok"}
privateEndpoint/api/privateТолько с Basic demo:secretПриветствие с именем пользователя

Цепочка запроса (как это «ощущается»)

HTTP → SecurityFilterChain → (аутентификация?) → DispatcherServlet → @RestController

Если фильтры вернули 401, ваш метод контроллера не вызовется — это и есть «защита по умолчанию».


Тест с MockMvc (опционально)

В spring-boot-starter-test уже есть Spring Security Test:

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
class ApiControllerTest {

@Autowired MockMvc mvc;

@Test
void publicOk() throws Exception {
mvc.perform(get("/api/public")).andExpect(status().isOk());
}

@Test
void privateUnauthorized() throws Exception {
mvc.perform(get("/api/private")).andExpect(status().isUnauthorized());
}

@Test
@WithMockUser(username = "demo")
void privateWithUser() throws Exception {
mvc.perform(get("/api/private")).andExpect(status().isOk());
}
}

Разбор теста MockMvc:

АннотацияЗачем
@SpringBootTestПоднимает полный контекст приложения
@AutoConfigureMockMvcВнедряет MockMvc — HTTP без реального сетевого порта
mvc.perform(get(...))Имитация GET-запроса
@WithMockUserПодставляет «вошедшего» пользователя без реального Basic

Так вы проверяете правила Security без ручного Base64 в каждом тесте.

Подробнее о тестах — 291.md.


Частые ошибки

СимптомПричина
Все эндпоинты 401Нет permitAll для нужных путей
401 с правильным паролемНеверный префикс {noop} или другой encoder
CSRF 403 на POSTДля API часто отключают CSRF или используют токен
Whitelabel 404Путь контроллера не совпадает с requestMatchers

Что попробовать

  1. Добавить роль ADMIN и правило .requestMatchers("/api/admin/**").hasRole("ADMIN").
  2. Вынести логин/пароль в application.yml (spring.security.user.name).
  3. Следующий уровень — JWT и Resource Server.

Дальше

JWT · Ошибки REST · Testcontainers · JPA · Spring Boot


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).