Spring Security 6.x 核心架构深度解析——认证链、过滤器链、安全上下文全貌
Spring Security 6.x 核心架构深度解析——认证链、过滤器链、安全上下文全貌
适读人群:用过Spring Security但对其架构感到困惑的Java后端开发者 | 阅读时长:约19分钟 | 核心价值:建立完整的Spring Security架构认知,不再靠"复制配置凑合用"
那段让我一度放弃的框架
2020年刚接触Spring Security,我的第一感受是:这是一个为了让你崩溃而设计的框架。
一个简单的登录功能,配置类写了200行,还是不知道为什么401了。加了@EnableWebSecurity,不知道为什么静态资源也被拦截了。想自定义登录逻辑,不知道该实现哪个接口、覆盖哪个方法。
后来我花了整整一个月,把Spring Security的源码从头到尾过了一遍,才真正搞明白它的架构。回过头来看,这个框架的设计其实非常精妙,只是文档写得太烂,很多关键概念藏得太深。
今天从架构层面把Spring Security 6.x的核心组件梳理清楚,以后遇到问题你知道该去哪里看。
核心架构:三层防御体系
Spring Security的架构可以分为三层:
HTTP请求
↓
┌─────────────────────────────────────┐
│ Filter Chain (Servlet过滤器链) │ 第一层:Servlet层过滤
│ DelegatingFilterProxy │
│ → FilterChainProxy │
│ → SecurityFilterChain[] │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ DispatcherServlet │ Spring MVC处理
│ → Controller │
│ → @PreAuthorize 方法安全 │ 第三层:方法级安全
└─────────────────────────────────────┘第一层:过滤器链
DelegatingFilterProxy 与 FilterChainProxy
Spring Security的入口是一个标准的Servlet Filter:DelegatingFilterProxy。
Servlet容器
└── DelegatingFilterProxy(Servlet Filter,名字固定为"springSecurityFilterChain")
└── 代理到 Spring Bean → FilterChainProxy
├── SecurityFilterChain #1: /api/** → [Filter1, Filter2, ...]
├── SecurityFilterChain #2: /admin/** → [Filter1, Filter2, ...]
└── SecurityFilterChain #3: /** → [完整的安全过滤链]FilterChainProxy持有多条SecurityFilterChain,根据请求URL匹配对应的链。每条链是独立的,可以有完全不同的安全配置(比如API接口用JWT,管理后台用Session)。
SecurityFilterChain 中的默认过滤器
一条典型的SecurityFilterChain包含以下过滤器(按执行顺序):
1. DisableEncodeUrlFilter // 禁止URL中的Session ID
2. WebAsyncManagerIntegrationFilter // 异步请求的安全上下文传播
3. SecurityContextHolderFilter // 从存储中加载SecurityContext
4. HeaderWriterFilter // 写安全相关的响应头
5. CorsFilter // CORS处理
6. CsrfFilter // CSRF防护
7. LogoutFilter // 处理退出
8. UsernamePasswordAuthenticationFilter // 处理表单登录
9. DefaultLoginPageGeneratingFilter // 生成默认登录页
10. DefaultLogoutPageGeneratingFilter // 生成默认退出页
11. BasicAuthenticationFilter // HTTP Basic认证
12. RequestCacheAwareFilter // 请求缓存
13. SecurityContextHolderAwareRequestFilter // 包装HttpServletRequest
14. AnonymousAuthenticationFilter // 设置匿名用户
15. ExceptionTranslationFilter // 将安全异常转换为HTTP响应
16. AuthorizationFilter // 最终的授权检查(替换了旧的FilterSecurityInterceptor)这个顺序非常重要:认证在授权之前,匿名用户填充在后,异常处理(ExceptionTranslationFilter)包裹授权检查(AuthorizationFilter)。
第二层:认证架构
认证的核心接口
// 认证信息的抽象,代表"谁在访问系统"
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities(); // 权限列表
Object getCredentials(); // 凭证(密码/Token等)
Object getDetails(); // 附加细节(IP地址、设备信息等)
Object getPrincipal(); // 身份主体(UserDetails对象或用户名)
boolean isAuthenticated(); // 是否已认证
}
// 认证的核心接口,负责"验证谁是谁"
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}ProviderManager:认证的委托中心
ProviderManager是AuthenticationManager的标准实现,它持有多个AuthenticationProvider,依次尝试:
// ProviderManager.authenticate() 核心逻辑(简化)
public Authentication authenticate(Authentication authentication) {
Class<? extends Authentication> toTest = authentication.getClass();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) continue; // 不支持这种认证类型,跳过
try {
Authentication result = provider.authenticate(authentication);
if (result != null) {
return result; // 认证成功
}
} catch (AccountStatusException | InternalAuthenticationServiceException e) {
throw e; // 账号状态异常或内部错误,直接抛出
} catch (AuthenticationException e) {
lastException = e; // 记录最后的异常,继续尝试下一个provider
}
}
// 所有provider都没成功
if (lastException == null) {
lastException = new ProviderNotFoundException("...");
}
throw lastException;
}默认的AuthenticationProvider:
DaoAuthenticationProvider:通过UserDetailsService加载用户,验证密码JwtAuthenticationProvider:验证JWT令牌- 可以自定义:短信验证码、微信授权等
UserDetailsService:用户数据加载
// 框架接口:根据用户名加载用户详情
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
// UserDetails 包含:用户名、密码(已加密)、权限列表、账号状态
public interface UserDetails {
String getUsername();
String getPassword();
Collection<? extends GrantedAuthority> getAuthorities();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}第三层:安全上下文
SecurityContextHolder:安全上下文的容器
// 安全上下文的存储中心
SecurityContextHolder
└── SecurityContext(通过 ThreadLocal 存储,默认策略)
└── Authentication(当前认证信息)
├── principal(UserDetails 或用户名)
├── credentials(已认证后通常为 null)
└── authorities(权限列表)// 获取当前用户信息
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
UserDetails user = (UserDetails) auth.getPrincipal();
String username = user.getUsername();
// 或者用 Spring Security 的注解直接注入
@GetMapping("/me")
public UserInfo me(@AuthenticationPrincipal UserDetails user) {
return new UserInfo(user.getUsername(), user.getAuthorities());
}SecurityContextRepository:安全上下文的持久化
默认情况下,SecurityContext存在HTTP Session里(通过HttpSessionSecurityContextRepository)。
对于无状态JWT认证,每次请求都要从Token重建SecurityContext,不存Session,配置为:
// JWT无状态配置
http.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
// Spring Security 6.x 会自动使用 RequestAttributeSecurityContextRepository
// 即SecurityContext只存在本次请求的RequestAttribute中,不持久化到SessionSpring Security 6.x 的重大变化
Spring Security 6.x(随Spring Boot 3.x发布)有几个重要变化:
1. SecurityFilterChain 取代 WebSecurityConfigurerAdapter
// Spring Security 5.x 旧写法(已废弃)
@Configuration
public class OldSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated();
}
}
// Spring Security 6.x 新写法(SecurityFilterChain Bean)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
}2. AuthorizationFilter 取代 FilterSecurityInterceptor
Spring Security 6.x 默认使用新的AuthorizationFilter,支持更灵活的授权模型(AuthorizationManager)。
3. 路径匹配器变化
// 5.x: antMatchers(已废弃)
.antMatchers("/api/**").hasRole("ADMIN")
// 6.x: requestMatchers
.requestMatchers("/api/**").hasRole("ADMIN")
// 注意:requestMatchers 使用 AntPathMatcher 或 PathPatternParser
// Spring Boot 3.x 默认用 PathPatternParser,两者的通配符行为略有不同完整配置代码:一个可运行的安全配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.*;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
/**
* Spring Security 6.x 完整配置示例
* 演示:多规则授权、表单登录、自定义UserDetailsService
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 开启方法级安全(支持@PreAuthorize等注解)
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 1. 请求授权规则(顺序很重要,先匹配先生效)
.authorizeHttpRequests(auth -> auth
// 静态资源和公开接口放行
.requestMatchers("/", "/login", "/register", "/css/**", "/js/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
// 管理接口需要ADMIN角色
.requestMatchers("/admin/**").hasRole("ADMIN")
// API接口需要认证(任意角色)
.requestMatchers("/api/**").authenticated()
// 其余请求都需要认证
.anyRequest().authenticated()
)
// 2. 表单登录配置
.formLogin(form -> form
.loginPage("/login") // 自定义登录页
.loginProcessingUrl("/doLogin") // 登录表单提交URL
.defaultSuccessUrl("/dashboard", true) // 登录成功跳转
.failureUrl("/login?error=true") // 登录失败跳转
.usernameParameter("username") // 表单字段名
.passwordParameter("password")
.permitAll()
)
// 3. 退出配置
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout=true")
.invalidateHttpSession(true) // 销毁Session
.clearAuthentication(true) // 清除认证信息
.deleteCookies("JSESSIONID") // 删除Cookie
.permitAll()
)
// 4. 记住我
.rememberMe(rememberMe -> rememberMe
.key("uniqueAndSecretKey")
.tokenValiditySeconds(7 * 24 * 3600) // 7天
)
// 5. Session管理
.sessionManagement(session -> session
.maximumSessions(1) // 同一账号最多1个并发Session
.maxSessionsPreventsLogin(false) // false=踢掉之前的Session,true=拒绝新登录
)
// 6. CSRF:API项目通常禁用(前后端分离用JWT)
// .csrf(AbstractHttpConfigurer::disable)
// 7. 异常处理
.exceptionHandling(ex -> ex
.accessDeniedPage("/403") // 权限不足跳转
);
return http.build();
}
/**
* 用户详情服务:从数据库加载用户
*/
@Bean
public UserDetailsService userDetailsService(UserRepository userRepository) {
return username -> {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPasswordHash())
.roles(user.getRoles().toArray(String[]::new))
.accountExpired(!user.isActive())
.accountLocked(user.isLocked())
.build();
};
}
/**
* 密码编码器:必须使用BCrypt,禁止明文存储
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // strength=12,每次hash约200ms,足够安全
}
}三个踩坑实录
坑一:requestMatchers 顺序错误导致规则失效
现象: 配置了/api/**需要认证,但请求不经过认证也能访问。
原因: 在/api/**之前配置了.anyRequest().permitAll(),anyRequest()是一个通配规则,匹配一切,后面的规则永远不会执行。
// 错误:anyRequest().permitAll() 在前,后面的规则无效
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll() // 先匹配这里,所有请求都放行
.requestMatchers("/api/**").authenticated() // 永远不会执行
)
// 正确:精确规则在前,anyRequest 在最后
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/**").authenticated() // 先匹配
.anyRequest().permitAll() // 兜底
)坑二:Spring Security 6 升级后 JSESSIONID 失效
现象: 从Spring Boot 2.x升级到3.x(Spring Security 6.x)后,Session认证失效,每次请求都要重新认证。
原因: Spring Security 6.x 默认启用了 requireExplicitSave(true),SecurityContext的保存变成了显式的,不再在每次请求结束后自动保存到Session。在某些自定义Filter里,直接设置了SecurityContext但没有保存,导致下次请求读不到。
解法:
// 在自定义Filter里,正确保存SecurityContext
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
// Spring Security 6.x 需要显式保存
securityContextRepository.saveContext(context, request, response);坑三:@EnableMethodSecurity 和 @Secured 注解不生效
现象: 加了@PreAuthorize("hasRole('ADMIN')")注解,但普通用户也能访问。
原因: 没有加@EnableMethodSecurity注解,或者加了但标注在了错误的配置类上(不在Spring的主配置扫描路径里)。
另一个常见原因:Spring Security代理是基于AOP的,如果在同一个Bean内部直接调用被注解的方法(自调用),AOP代理不生效。
// 错误:自调用,@PreAuthorize 不生效
@Service
public class UserService {
public void publicMethod() {
adminMethod(); // 直接调用,绕过AOP代理
}
@PreAuthorize("hasRole('ADMIN')")
public void adminMethod() { ... }
}
// 正确:通过Spring容器的代理对象调用
@Service
public class UserService {
@Autowired
private UserService self; // 注入自己(通过代理)
public void publicMethod() {
self.adminMethod(); // 通过代理调用,@PreAuthorize 生效
}
}小结
Spring Security 6.x 的架构可以用三句话概括:
- 过滤器链是入口:所有请求先经过
FilterChainProxy,按URL匹配到对应的SecurityFilterChain - ProviderManager是认证中枢:委托给多个
AuthenticationProvider,支持多种认证方式 - SecurityContextHolder是信息中心:认证通过后,用户信息存这里,整个请求处理链都可以访问
理解了这三层架构,遇到任何Spring Security的问题,你都知道该去哪里找原因,该在哪里做扩展。
