Skip to content
认证与授权工作原理
概述
Spring Security 的运行时入口是一组 Servlet Filter,而不是开发者编写的 HttpSecurity 配置类。请求经过 Servlet 容器注册的 DelegatingFilterProxy,流入 FilterChainProxy,再由后者委托给多条 SecurityFilterChain 完成认证与授权。全过程依赖 SecurityContextHolder 在线程内传递身份信息,ProviderManager 负责串联多个 AuthenticationProvider,而 UsernamePasswordAuthenticationToken 则是用户名密码场景中最常见的认证凭证对象。
基本概念
SecurityContextHolder
持有当前线程安全上下文(SecurityContext)的工具类。默认策略是 MODE_THREADLOCAL,即每个线程维护一份独立的上下文副本。认证成功后,经过验证的 Authentication 对象会被写入这个上下文:
java
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();后续授权、控制器方法注入等操作,本质上都是从这个上下文中取值,而不是重新查询数据库。
Authentication
表示一次认证请求的凭证或已认证主体的令牌。主要字段包括:principal(通常是用户标识)、credentials(凭证,如密码,认证成功后会被擦除)、authorities(授予的权限)。
SecurityContext
Authentication 对象的容器,仅提供 get/set 存取方式。它本身不涉及序列化或网络传递,只在当前线程内有效。
UserDetails
UserDetailsService 返回的用户信息结构。包含用户名、密码、账户状态(是否过期、锁定)以及 GrantedAuthority 集合。它是 DaoAuthenticationProvider 用来做密码比对的数据来源。
工作原理
从代理到真正的过滤器链
Servlet 容器只能理解 javax.servlet.Filter 接口。为了让 Spring Security 的过滤逻辑接入容器,标准做法是在 web.xml(或 Spring Boot 自动配置)中注册一个 DelegatingFilterProxy。这个代理本身的 doFilter 实现非常简单:从 Spring 容器中查找名为 springSecurityFilterChain 的 Bean,并把请求委托给它。
springSecurityFilterChain 的实际类型是 FilterChainProxy,它是整个安全过滤的入口。
FilterChainProxy 与多条过滤器链
FilterChainProxy 内部维护一个 List<SecurityFilterChain>。当请求到达时,遍历这些链,用每条链绑定的 RequestMatcher 去匹配当前请求,仅选择第一条匹配的链执行。
每条 SecurityFilterChain 包含一组 Filter 实例,常见的有:
SecurityContextPersistenceFilterCsrfFilterAuthenticationFilter(如UsernamePasswordAuthenticationFilter)AuthorizationFilterExceptionTranslationFilter
这种设计将匹配逻辑与过滤逻辑分离,允许针对不同 URL 模式配置完全不同的过滤器集合。一个应用内可以同时定义多条 SecurityFilterChain,分别处理 API 接口、管理后台和静态资源。
认证流程:ProviderManager 与 DaoAuthenticationProvider
认证管理的默认实现是 ProviderManager,它实现了 AuthenticationManager 接口。ProviderManager 持有一个 List<AuthenticationProvider>,遍历每个 Provider,调用其 supports(Class<?> authentication) 确认能否处理当前传入的 Authentication 对象,找到后调用 authenticate 方法。
以用户名密码认证为例,涉及到的 Provider 是 DaoAuthenticationProvider:
- 从接收到的
UsernamePasswordAuthenticationToken中提取用户名。 - 调用注入的
UserDetailsService.loadUserByUsername(username)获取UserDetails。 - 使用
PasswordEncoder.matches(rawPassword, encodedPassword)比对密码。 - 比对成功后,构造一个新的
UsernamePasswordAuthenticationToken,将principal设为UserDetails对象,authorities设为从UserDetails提取的权限列表,并标记为已认证(authenticated = true)。 - 将已认证的
Authentication返回给调用方,后者将其写入SecurityContextHolder。
授权流程:AuthorizationFilter 与 AuthorizationManager
Spring Security 6 中,默认的授权过滤器是 AuthorizationFilter。它本身不直接判断是否有权限,而是将当前 Authentication、当前 HttpServletRequest 以及待保护资源的信息打包,交给一个 AuthorizationManager<RequestAuthorizationContext> 实例进行决策。
AuthorizationManager 的典型实现是 RequestMatcherDelegatingAuthorizationManager。它的工作方式是:
- 维护一个
List<RequestMatcherEntry<AuthorizationManager>>。 - 依次将请求与每个条目的
RequestMatcher匹配。 - 投递到对应的
AuthorizationManager上执行check方法。
这个 check 方法返回值是 AuthorizationDecision,包含是否允许(granted)的标志。如果所有条目都不匹配,通常会返回一个拒绝决定或交由后继处理器。
例如,配置了 http.authorizeHttpRequests(auth -> auth.requestMatchers("/admin/**").hasRole("ADMIN").anyRequest().authenticated()) 后,内部会生成两个条目:第一个匹配 /admin/** 且要求 ROLE_ADMIN,第二个匹配其余所有请求并要求已认证。在决策时,第一个匹配到的条目直接决定结果,不再继续尝试后续条目。
基本用法
获取 AuthenticationManager
在 Spring Security 6 中,可以通过 AuthenticationConfiguration 获取共享的 AuthenticationManager:
java
@Bean
AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}该方法会收集所有 Bean 容器中的 AuthenticationProvider,自动组装成一个 ProviderManager。
自定义 UserDetailsService
实现 UserDetailsService 并将其标注为 @Service,Spring Security 会自动检测并注入到 DaoAuthenticationProvider 中:
java
@Service
class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return org.springframework.security.core.userdetails.User.withUsername(username)
.password("{noop}123456")
.authorities("ROLE_USER")
.build();
}
}{noop} 前缀告诉 PasswordEncoder 该密码为明文。通常应使用 BCryptPasswordEncoder 等强哈希算法,此时存储的密码应为 BCrypt 密文,且无需前缀。
配置请求授权
使用 HttpSecurity 的 authorizeHttpRequests 方法配置授权规则:
java
@Bean
SecurityFilterChain webSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated());
return http.build();
}该方法内部会将声明的匹配规则转换成 RequestMatcherDelegatingAuthorizationManager 中的条目,供 AuthorizationFilter 在每次请求时使用。
手动执行认证
在控制器或服务中可以编程触发认证:
java
AuthenticationManager manager = ...;
UsernamePasswordAuthenticationToken unauthenticated =
new UsernamePasswordAuthenticationToken(username, password);
Authentication result = manager.authenticate(unauthenticated);
SecurityContextHolder.getContext().setAuthentication(result);authenticate 返回的是已认证的新令牌(新旧对象不相同,旧对象的 authenticated 为 false)。
示例:请求全链路日志
下面是一次经过表单登录的请求在调试日志中的关键片段,展示了从认证到授权的状态变化:
text
DEBUG o.s.s.w.a.UsernamePasswordAuthenticationFilter: Request is to process authentication
DEBUG o.s.s.authentication.ProviderManager: Authenticating request with DaoAuthenticationProvider
DEBUG o.s.s.a.d.DaoAuthenticationProvider: User 'user' found
DEBUG o.s.s.a.d.DaoAuthenticationProvider: Password matched
DEBUG o.s.s.w.a.UsernamePasswordAuthenticationFilter: Authentication success. Updating SecurityContextHolder
DEBUG o.s.s.w.c.SecurityContextPersistenceFilter: SecurityContextHolder now contains Authentication
DEBUG o.s.s.w.a.AuthorizationFilter: Checking authorization for request '/admin/dashboard'
DEBUG o.s.s.a.i.RequestMatcherDelegatingAuthorizationManager: Checking match for '/admin/**' with hasRole('ROLE_ADMIN')
DEBUG o.s.s.a.i.RequestMatcherDelegatingAuthorizationManager: Granted access认证由 UsernamePasswordAuthenticationFilter 拦截;ProviderManager 委派 DaoAuthenticationProvider 完成用户查找与密码比对;成功后立即将 Authentication 写入上下文;后续 AuthorizationFilter 通过 RequestMatcherDelegatingAuthorizationManager 匹配到对应的规则并返回授权通过。
注意点
线程池复用与上下文清理
SecurityContextHolder 默认绑定线程,在同一个线程中,后续的安全相关操作都共享该上下文。当使用线程池时,线程会被复用,如果不在任务完成后清理上下文,就可能造成用户串号。
Spring Security 提供的 DelegatingSecurityContextRunnable 或 DelegatingSecurityContextExecutor 可以自动传递和恢复上下文。但如果自行管理线程,则需要显式调用 SecurityContextHolder.clearContext() 来清理。
密码编码器前缀
存储密码时,若使用 {id}encodedPassword 格式,则必须确保注入的 PasswordEncoder 是 DelegatingPasswordEncoder 或支持此形式的实现。直接使用 BCryptPasswordEncoder 而不带前缀时,密码存储方式应与编码器匹配,否则认证过程会抛出异常。
认证对象的信息擦除
ProviderManager 在认证成功后默认会擦除 Authentication 的 credentials 字段(如密码),防止敏感信息在内存中留存过久。这是通过调用 authentication.eraseCredentials() 完成的。自定义 Provider 实现也应当遵循这一惯例。
多条 SecurityFilterChain 的顺序
FilterChainProxy 按 SecurityFilterChain 注册顺序进行匹配,一旦某条链的 RequestMatcher 匹配成功,剩余链不再考虑。因此,更具体的匹配规则(如 /api/user/**)应当放在较通用的规则(如 /api/**)之前。
参考链接
- Spring Security 官方文档
- Spring Security Architecture 官方说明
FilterChainProxy源码(org.springframework.security.web.FilterChainProxy)ProviderManager源码(org.springframework.security.authentication.ProviderManager)DaoAuthenticationProvider源码(org.springframework.security.authentication.dao.DaoAuthenticationProvider)RequestMatcherDelegatingAuthorizationManager源码(org.springframework.security.authorization.RequestMatcherDelegatingAuthorizationManager)
