Spring Security 认证流程深度
概述
Spring Security 的认证流程围绕 Authentication 接口展开:请求携带凭证(用户名密码、短信验证码、OAuth2 Token 等),经过一系列 Filter 和 Provider 的处理,最终填充到 SecurityContext。
核心接口
| 接口 | 职责 |
|---|---|
Authentication | 认证对象,包含 principal(主体)、credentials(凭证)、authorities(权限) |
AuthenticationManager | 认证管理器,核心方法 authenticate() |
ProviderManager | AuthenticationManager 的默认实现,维护 AuthenticationProvider 链 |
AuthenticationProvider | 具体的认证逻辑,支持不同类型的 Authentication 令牌 |
SecurityContextHolder | 持有当前线程的 SecurityContext |
SecurityContext | 持有已认证的 Authentication 对象 |
一、SecurityContextHolder 存储策略
java
// 3 种存储策略
SecurityContextHolder.setStrategyName(
SecurityContextHolder.MODE_THREADLOCAL // 默认,每个线程独立
// SecurityContextHolder.MODE_INHERITABLETHREADLOCAL // 子线程继承
// SecurityContextHolder.MODE_GLOBAL // 全局单例
);
// 获取当前用户
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();异步场景注意事项:
java
// 方案 1:手动传递
ExecutorService executor = ...;
SecurityContext context = SecurityContextHolder.getContext();
executor.submit(() -> {
SecurityContextHolder.setContext(context);
// 执行业务
SecurityContextHolder.clearContext();
});
// 方案 2:使用 DelegatingSecurityContextRunnable / AsyncTaskExecutor
@Bean
public Executor taskExecutor() {
return new DelegatingSecurityContextExecutorService(
Executors.newFixedThreadPool(10)
);
}二、AbstractAuthenticationProcessingFilter 源码
AbstractAuthenticationProcessingFilter 是表单登录认证过滤器的基类,UsernamePasswordAuthenticationFilter 是其最常用的子类。
2.1 核心流程
java
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response); // 不是登录请求,放行
return;
}
Authentication authResult;
try {
// 1. 创建 Authentication 令牌(未认证状态)
authResult = attemptAuthentication(request, response);
if (authResult == null) {
return; // 子类返回 null 表示认证尚未完成
}
// 2. Session 固定攻击防护
sessionStrategy.onAuthentication(authResult, request, response);
// 3. 认证成功回调
successfulAuthentication(request, response, chain, authResult);
} catch (AuthenticationException e) {
// 4. 认证失败回调
unsuccessfulAuthentication(request, response, e);
}
}2.2 UsernamePasswordAuthenticationFilter
java
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("仅支持 POST 请求");
}
// 从请求中提取用户名和密码
String username = obtainUsername(request);
String password = obtainPassword(request);
// 创建未认证的 UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authRequest =
UsernamePasswordAuthenticationToken.unauthenticated(username, password);
// 允许子类自定义细节
setDetails(request, authRequest);
// 委托给 AuthenticationManager
return this.getAuthenticationManager().authenticate(authRequest);
}2.3 认证成功与失败
java
// 认证成功
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult) {
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
// 默认跳转到 savedRequest 或首页
successHandler.onAuthenticationSuccess(request, response, authResult);
}
// 认证失败
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed) {
SecurityContextHolder.clearContext();
rememberMeServices.loginFail(request, response);
failureHandler.onAuthenticationFailure(request, response, failed);
}三、ProviderManager 与 AuthenticationProvider 链
3.1 ProviderManager 源码
java
public Authentication authenticate(Authentication authentication) {
Class<? extends Authentication> toTest = authentication.getClass();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue; // 跳过不支持的 Provider
}
try {
Authentication result = provider.authenticate(authentication);
if (result != null) {
// 复制 details
copyDetails(authentication, result);
// 如果 parent 存在,允许 parent 补充权限
if (this.parentResult != null) {
result = this.parentResult;
}
return result;
}
} catch (AuthenticationException e) {
// 继续尝试下一个 Provider
lastException = e;
}
}
// 所有 Provider 都失败
if (lastException == null) {
lastException = new ProviderNotFoundException("No AuthenticationProvider found for " + toTest.getName());
}
throw lastException;
}3.2 内置 AuthenticationProvider
| Provider | 支持令牌 | 用途 |
|---|---|---|
DaoAuthenticationProvider | UsernamePasswordAuthenticationToken | 用户名密码认证(默认) |
AnonymousAuthenticationProvider | AnonymousAuthenticationToken | 匿名用户 |
RememberMeAuthenticationProvider | RememberMeAuthenticationToken | 记住我认证 |
OAuth2LoginAuthenticationProvider | OAuth2LoginAuthenticationToken | OAuth2 登录 |
CasAuthenticationProvider | CasAuthenticationToken | CAS 单点登录 |
3.3 Provider 匹配机制
ProviderManager 持有 Provider 列表:
[DaoAuthenticationProvider, OAuth2LoginAuthenticationProvider, ...]
│
▼
收到 UsernamePasswordAuthenticationToken 令牌
│
▼
遍历 Provider 列表 → 调用 supports() 判断是否匹配
DaoAuthenticationProvider.supports(UsernamePasswordAuthenticationToken) → true
→ 执行 authenticate()四、自定义 AuthenticationProvider
4.1 短信验证码认证
java
// 1. 自定义令牌
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal; // 手机号
private Object credentials; // 验证码
public SmsCodeAuthenticationToken(String phone, String code) {
super(null);
this.principal = phone;
this.credentials = code;
setAuthenticated(false);
}
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() { return credentials; }
@Override
public Object getPrincipal() { return principal; }
}java
// 2. 自定义 Provider
@Component
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private SmsCodeService smsCodeService;
@Override
public Authentication authenticate(Authentication authentication) {
String phone = authentication.getName();
String code = (String) authentication.getCredentials();
// 验证短信验证码
if (!smsCodeService.validate(phone, code)) {
throw new BadCredentialsException("短信验证码错误");
}
// 加载用户信息
UserDetails user = userDetailsService.loadUserByUsername(phone);
// 返回已认证的令牌
return new SmsCodeAuthenticationToken(user, user.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}java
// 3. 自定义 Filter
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/login/sms", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) {
String phone = request.getParameter("phone");
String code = request.getParameter("code");
SmsCodeAuthenticationToken authRequest =
new SmsCodeAuthenticationToken(phone, code);
return this.getAuthenticationManager().authenticate(authRequest);
}
}4.2 注册自定义 Provider
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authenticationProvider(smsCodeAuthenticationProvider())
.addFilterBefore(smsCodeAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login/sms").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}五、记住我(RememberMe)
5.1 RememberMeServices
java
public interface RememberMeServices {
Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
void loginFail(HttpServletRequest request, HttpServletResponse response);
void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication);
}5.2 配置
java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.rememberMe(remember -> remember
.key("unique-and-secret") // 加密密钥
.tokenValiditySeconds(7 * 24 * 3600) // 7 天有效
.rememberMeParameter("remember-me") // 表单参数名
.userDetailsService(userDetailsService)
.authenticationSuccessHandler(rememberMeSuccessHandler())
);
return http.build();
}5.3 数据库持久化
java
@Bean
public PersistentTokenRepository persistentTokenRepository(DataSource dataSource) {
JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
repo.setDataSource(dataSource);
repo.setCreateTableOnStartup(true); // 首次自动建表
return repo;
}六、实战:第三方账号扫码登录
6.1 流程
用户点击「微信扫码登录」
│
▼
前端展示二维码 → 轮询查询登录状态
│
▼
用户扫码 → 微信回调我们的服务
│
▼
回调接口回调 → 根据 openId 创建/查询用户
│
▼
生成临时 token → 前端轮询到后写入 SecurityContext6.2 第三方认证 Provider
java
@Component
public class OAuth2AuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserService userService;
@Override
public Authentication authenticate(Authentication authentication) {
OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication;
String provider = token.getProvider(); // wechat / alipay
String openId = token.getOpenId();
// 查找或创建用户
User user = userService.findOrCreateByOpenId(provider, openId);
// 构建权限
List<GrantedAuthority> authorities = AuthorityUtils
.commaSeparatedStringToAuthorityList("ROLE_USER");
UsernamePasswordAuthenticationToken authenticated =
UsernamePasswordAuthenticationToken.authenticated(user, null, authorities);
authenticated.setDetails(token.getDetails());
return authenticated;
}
@Override
public boolean supports(Class<?> authentication) {
return OAuth2AuthenticationToken.class.isAssignableFrom(authentication);
}
}七、总结
| 知识点 | 要点 |
|---|---|
| SecurityContextHolder | ThreadLocal 存储,子线程需手动传递或 DelegatingSecurityContextExecutor |
| AbstractAuthenticationProcessingFilter | requiresAuthentication() → attemptAuthentication() → 成功/失败回调 |
| UsernamePasswordAuthenticationFilter | 提取 username/password → 创建令牌 → 委托 ProviderManager |
| ProviderManager | 遍历 AuthenticationProvider 链,通过 supports() 匹配 |
| 自定义 Provider | 实现 AuthenticationProvider 接口 + 自定义 Authentication 令牌 |
| 记住我 | RememberMeServices 接口,支持 Cookies 和数据库持久化 |
| 自定义 Filter | 继承 AbstractAuthenticationProcessingFilter,处理非表单登录 |
参考链接: