第2章:Spring Security基础:FilterChain原理、SecurityContextHolder、AuthenticationManager
好,咱们直接进入正题。这一章我会带你拆解Spring Security最核心的三个概念。说实话,我当年刚接触Spring Security时,就是被这三个东西搞得晕头转向。但搞懂了它们,后面的路就顺了。
2.1 FilterChain:一切从这里开始
Spring Security本质上是一堆过滤器组成的链条。你想想看,一个HTTP请求进来,总得经过一道道检查吧?谁认证了?有没有权限?CSRF令牌对不对?这些检查,就是由一个个Filter完成的。
我习惯把FilterChain想象成机场安检。你过安检时,先查身份证,再查登机牌,最后查行李。任何一个环节出问题,你就被拦下了。Spring Security的FilterChain也是这个道理。
核心要点:Spring Security通过DelegatingFilterProxy将请求委托给一个名为springSecurityFilterChain的过滤器链。这个链里默认有十几个过滤器,每个负责一件事。
举个例子,常见的过滤器包括:
SecurityContextPersistenceFilter:从Session中读取SecurityContextUsernamePasswordAuthenticationFilter:处理表单登录BasicAuthenticationFilter:处理HTTP Basic认证ExceptionTranslationFilter:处理认证异常FilterSecurityInterceptor:最后的权限裁决者
它们的顺序是固定的。为什么?因为有些过滤器依赖前面的结果。比如FilterSecurityInterceptor必须等SecurityContextPersistenceFilter把用户信息准备好,才能做权限判断。
我的经验:我曾经在项目中自定义了一个过滤器,想放在最前面做IP白名单校验。结果忘了设置顺序,导致请求先被认证过滤器拦截了。嗯,后来我学乖了,自定义过滤器时一定用@Order或addFilterBefore/addFilterAfter明确位置。
2.2 SecurityContextHolder:用户信息的“临时仓库”
说白了,SecurityContextHolder就是一个ThreadLocal。它把当前请求的用户认证信息存在当前线程里。这样你在业务代码里随时能拿到当前用户是谁。
怎么拿?很简单:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
这里有个坑,我必须要说。默认情况下,SecurityContextHolder使用MODE_THREADLOCAL策略。这意味着每个线程都有自己的SecurityContext副本。如果你的应用用了异步处理,比如@Async或者线程池,子线程是拿不到父线程的认证信息的。
注意:异步场景下,SecurityContext不会自动传递。你需要手动设置,或者用SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL)。我有个同事就因为这个bug排查了一整天,请求进来时明明登录了,异步方法里却拿不到用户信息。
另外,SecurityContextHolder还支持MODE_GLOBAL模式,所有线程共享一个SecurityContext。但说实话,我几乎没用过,因为多用户场景下会乱套。
2.3 AuthenticationManager:认证的“总指挥”
AuthenticationManager是一个接口,只有一个方法:authenticate(Authentication authentication)。它的职责很简单——接收一个未认证的Authentication对象,返回一个已认证的Authentication对象。如果认证失败,就抛出异常。
它的默认实现是ProviderManager。ProviderManager内部维护了一个List<AuthenticationProvider>。它会挨个问这些Provider:“你能认证这个请求吗?”直到有一个Provider说“我能”,并且认证成功。
为什么会设计成多个Provider?因为一个应用可能有多种认证方式。比如:
- 用户名密码登录 →
DaoAuthenticationProvider - LDAP认证 →
LdapAuthenticationProvider - JWT令牌认证 → 自定义Provider
每个Provider只负责一种认证方式。这样设计的好处是——高内聚、低耦合。你加一种新认证方式,只需要新增一个Provider,不用改现有代码。
实战建议:我在项目中经常自定义AuthenticationProvider。比如对接第三方登录时,我会写一个ThirdPartyAuthenticationProvider,在里面调用第三方API验证token,然后构建一个已认证的Authentication对象返回。
来看一个自定义Provider的骨架:
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
// 这里写你的认证逻辑
// 比如查数据库、调远程接口等
if (认证通过) {
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
return new UsernamePasswordAuthenticationToken(
username, password, authorities);
} else {
throw new BadCredentialsException("用户名或密码错误");
}
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication);
}
}
注意supports方法。它告诉ProviderManager:我这个Provider只处理UsernamePasswordAuthenticationToken类型的认证请求。如果类型不匹配,ProviderManager就跳过它,问下一个Provider。
2.4 三者如何协作?
我画个简单的流程,你感受一下:
- 用户发来一个HTTP请求
- 请求进入FilterChain,经过一系列过滤器
- 某个过滤器(比如
UsernamePasswordAuthenticationFilter)从请求中提取用户名密码,构建一个未认证的UsernamePasswordAuthenticationToken - 过滤器调用
AuthenticationManager.authenticate() ProviderManager遍历它的Provider列表,找到能处理这个Token的Provider- Provider执行认证逻辑,返回已认证的Authentication对象
- 过滤器把认证结果存入
SecurityContextHolder - 后续过滤器(比如
FilterSecurityInterceptor)从SecurityContextHolder中获取用户信息,做权限判断
你看,整个流程环环相扣。FilterChain负责拦截请求,AuthenticationManager负责认证,SecurityContextHolder负责存储认证结果。三者缺一不可。
避坑指南:我曾经在微服务项目中,忘了在网关层配置SecurityContextPersistenceFilter,导致每次请求进来SecurityContext都是空的。用户明明登录了,但每个请求都要重新认证。后来我在网关层加了一个过滤器,手动从JWT中解析用户信息并设置到SecurityContextHolder中,问题才解决。
2.5 小结
这一章我们聊了三个核心概念:
- FilterChain:Spring Security的安检通道,每个过滤器负责一道检查
- SecurityContextHolder:用户信息的临时仓库,基于ThreadLocal实现
- AuthenticationManager:认证总指挥,通过ProviderManager和多个AuthenticationProvider完成认证
搞懂这三样,你就掌握了Spring Security的骨架。下一章我们会深入UsernamePasswordAuthenticationFilter,看看表单登录到底是怎么工作的。到时候你会发现,原来登录流程就是这一章知识的实际应用。
嗯,今天就到这儿。有什么问题,欢迎交流。