文档站点
Skip to content

Spring Security 深入 ​

前面几篇我们把认证和授权跑通了,但 Spring Security 的「过滤器链」机制还有不少反直觉的地方。这篇把架构补全,让你遇到问题时不至于一头雾水。

架构总览:过滤器链 + 关键组件 ​

请求进入
  → [过滤器链](一条链上挂 N 个 Filter)
      ├─ SecurityContextPersistenceFilter   恢复/保存登录态
      ├─ CsrfFilter                          防御 CSRF
      ├─ UsernamePasswordAuthenticationFilter 表单登录(传统 MVC)
      ├─ AnonymousAuthenticationFilter      未登录也建「匿名认证」
      ├─ ExceptionTranslationFilter         捕获异常转 401/403/重定向
      └─ AuthorizationFilter                最后的授权检查(anyRequest)
  → Controller

三个核心概念

组件作用Laravel 对应
SecurityFilterChain一整条过滤器链,可按路径配置多条中间件栈
SecurityContext当前请求的认证信息(ThreadLocal)Auth 全局状态
AuthenticationManager认证「收银台」,验证凭证Auth::attempt 的底层

理解「过滤器链」就够了:Spring Security 不是一个注解,而是一套请求拦截体系,认证信息存 ThreadLocal(SecurityContext),每请求自动清理。

SecurityContext 是 ThreadLocal ​

java
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
  • 每个请求进来时 SecurityContextPersistenceFilter 从 Session/ThreadLocal 恢复,结束时清空
  • 所以一个请求内的任意代码都能拿到当前用户(Controller、Service 都行)
  • 但如果手动开新线程(new Thread、@Async),ThreadLocal 不会自动带过去 —— 异步任务里拿不到认证信息是常见坑

WARNING

@Async 异步方法里 SecurityContextHolder 是空的。解决:在进入异步前把用户 ID 作为参数传进去(推荐),或手动传播 SecurityContext。这和 Laravel 里「Job 里拿不到 Auth::user() 要自己传」是同一个问题。

多过滤器链:前后端分离怎么配 ​

一个系统可能同时有「无状态 API」和「传统表单登录页面」,可以配多条 SecurityFilterChain,按路径分流:

java
@Configuration
public class MultiSecurityConfig {

    @Bean
    @Order(1)                      // 优先级高,先匹配
    public SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
        http.securityMatcher("/api/**")          // 只处理 /api/**
            .csrf(csrf -> csrf.disable())
            .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/api/auth/**").permitAll()
                    .anyRequest().authenticated());
        return http.build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain webChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
        return http.build();
    }
}

TIP

@Order(1) 的链先试;securityMatcher("/api/**") 匹配到了就用这条链,不再往下找。对应 Laravel 里「admin 路由用一组中间件,api 路由用另一组」。

常见自定义点 ​

自定义 401 / 403 响应(前后端分离必做) ​

java
http.exceptionHandling(ex -> ex
        .authenticationEntryPoint((req, res, e) -> {
            res.setStatus(401);
            res.setContentType("application/json;charset=UTF-8");
            res.getWriter().write("{\"code\":401,\"message\":\"未登录或登录已过期\"}");
        })
        .accessDeniedHandler((req, res, e) -> {
            res.setStatus(403);
            res.setContentType("application/json;charset=UTF-8");
            res.getWriter().write("{\"code\":403,\"message\":\"没有权限\"}");
        }));
组件时机Laravel 对应
AuthenticationEntryPoint未登录访问受保护资源中间件里 redirect('/login')
AccessDeniedHandler已登录但无权限Gate::denies 后的 403

放行静态资源 ​

java
.authorizeHttpRequests(auth -> auth
        .requestMatchers("/", "/static/**", "/favicon.ico", "/actuator/health").permitAll()
        .anyRequest().authenticated());

CORS(前后端分离必配) ​

java
@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("http://localhost:5173"));
    config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    config.setAllowedHeaders(List.of("*"));
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return source;
}

// 链里启用
http.cors(cors -> cors.configurationSource(corsConfigurationSource()));

TIP

配了 http.cors(...) 后,Spring Security 的 CorsFilter 会先于其他过滤器处理 OPTIONS 预检请求,否则前端跨域直接 403。这是前后端分离最常见的坑。

默认行为会让新手懵的点 ​

现象原因
引入 starter 后所有接口都被保护anyRequest().authenticated() 是默认兜底,且自动生成登录页
启动日志里有个「随机密码」自动生成用户 user + 随机密码(开发期默认)
接口突然 401没带 Authorization 头,或 token 无效/过期
返回 302 而不是 401默认配置走的是「跳转登录页」,API 要改成上面的自定义 entry point

关闭默认保护(仅限极简 demo)

yaml
spring:
  security:
    user:
      name: user
      password: 123456

配了用户名密码,内置表单登录就用这个账号。真要「全部放行」可配 permitAll,但生产别这么做。

调试技巧 ​

  1. 加依赖 spring-boot-starter-actuator,暴露 http.trace
  2. 日志级别打开过滤器链:
yaml
logging:
  level:
    org.springframework.security: DEBUG

会打印每个请求经过了哪些 Filter、认证结果如何,排障神器。

从 Laravel 视角的最终总结 ​

Laravel 习惯Spring Security 对应
中间件栈 web / api多条 SecurityFilterChain
auth 中间件过滤器链的授权检查
Auth::user()SecurityContextHolder / Authentication
登录/登出路由自定义 Controller 或内置表单登录
Gate / Policy@PreAuthorize + 自定义 SpEL
401 / 403 定制AuthenticationEntryPoint / AccessDeniedHandler

一句话总结:Spring Security = 「可插拔的过滤器链 + 可替换的认证/授权组件」,配置在一个 SecurityFilterChain Bean 里集中完成。先把 认证 和 授权 的示例跑通,再回来读这篇深入原理。

面向 PHP / Laravel 开发者的 Spring Boot 中文文档