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
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,按路径分流:
@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 响应(前后端分离必做)
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 |
放行静态资源
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/static/**", "/favicon.ico", "/actuator/health").permitAll()
.anyRequest().authenticated());CORS(前后端分离必配)
@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)
spring:
security:
user:
name: user
password: 123456配了用户名密码,内置表单登录就用这个账号。真要「全部放行」可配 permitAll,但生产别这么做。
调试技巧
- 加依赖
spring-boot-starter-actuator,暴露http.trace - 日志级别打开过滤器链:
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 里集中完成。先把 认证 和 授权 的示例跑通,再回来读这篇深入原理。