Spring BootSeniorTechnical

Как защитить REST API на Spring Boot с помощью Spring Security?

Настраивается через SecurityFilterChain: CSRF отключается, SessionCreationPolicy.STATELESS, правила authorizeHttpRequests по ролям, @EnableMethodSecurity для @PreAuthorize, кастомный AuthenticationEntryPoint возвращает 401 вместо редиректа.

Минимальная конфигурация SecurityFilterChain

@Configuration
@EnableWebSecurity
@EnableMethodSecurity   // включает @PreAuthorize, @PostAuthorize
public class ApiSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            // REST API не использует сессии и CSRF-куки
            .csrf(AbstractHttpConfigurer::disable)
            .sessionManagement(s ->
                s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            // правила авторизации
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(HttpMethod.GET,  "/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            // возврат 401/403 вместо редиректа на /login
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint((req, res, e) ->
                    res.sendError(HttpServletResponse.SC_UNAUTHORIZED))
                .accessDeniedHandler((req, res, e) ->
                    res.sendError(HttpServletResponse.SC_FORBIDDEN))
            )
            .build();
    }
}

RBAC через @PreAuthorize

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @GetMapping
    @PreAuthorize("hasAnyRole('USER','ADMIN')")
    public List<Order> list() { ... }

    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN') or @orderService.isOwner(#id, authentication.name)")
    public void delete(@PathVariable UUID id) { ... }
}

Защита от распространённых атак

Rate limiting — через Bucket4j + Redis:

@Component
public class RateLimitFilter extends OncePerRequestFilter {
    private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();

    @Override
    protected void doFilterInternal(HttpServletRequest req,
                                    HttpServletResponse res,
                                    FilterChain chain)
            throws ServletException, IOException {
        String ip = req.getRemoteAddr();
        Bucket bucket = buckets.computeIfAbsent(ip, k ->
            Bucket.builder()
                .addLimit(Bandwidth.classic(100,
                    Refill.greedy(100, Duration.ofMinutes(1))))
                .build());
        if (bucket.tryConsume(1)) {
            chain.doFilter(req, res);
        } else {
            res.setStatus(429);
        }
    }
}

Security headers через application.yml или конфигурацию:

.headers(h -> h
    .contentSecurityPolicy(csp ->
        csp.policyDirectives("default-src 'self'"))
    .frameOptions(HeadersConfigurer.FrameOptionsConfig::deny)
    .httpStrictTransportSecurity(hsts ->
        hsts.includeSubDomains(true).maxAgeInSeconds(31536000))
)

Настройка CORS для фронтенда

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("https://app.example.com"));
    config.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
    config.setAllowedHeaders(List.of("Authorization","Content-Type"));
    config.setAllowCredentials(true);
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/api/**", config);
    return source;
}

Подводные камни

  • Отключать CSRF для REST правильно только при STATELESS-сессиях; если вы используете cookie-сессии — CSRF нужен.
  • Порядок правил в authorizeHttpRequests важен: более специфичные паттерны должны идти раньше anyRequest().
  • @EnableMethodSecurity не работает без проксирования — вызов защищённого метода из того же бина обходит проверку.
  • Без кастомного AuthenticationEntryPoint Spring Security редиректит на /login — для REST API это 302, а не 401.
  • CORS-конфигурация в Spring Security и в @CrossOrigin могут конфликтовать — используйте один источник истины.
  • Логировать тело запроса в фильтре безопасности рискованно — можно случайно залогировать пароль или токен.
  • Actuator-эндпоинты (/actuator/**) нужно явно закрыть или ограничить ролью ACTUATOR.

Common mistakes

  • Путать термин «spring security rest api» с соседним механизмом Spring Boot.
  • Не называть границу lifecycle, transaction, thread или request для «spring security rest api».
  • Игнорировать production-эффекты «spring security rest api»: latency, SQL shape, memory, security или observability.

What the interviewer is testing

  • Попросить объяснить механизм «spring security rest api» на минимальном примере.
  • Проверить, видит ли кандидат failure mode и диагностику для «spring security rest api».
  • Уточнить, какие настройки или API меняют «spring security rest api» в реальном сервисе.

Sources

Related topics