Spring Security源码深度解析:从FilterChainProxy到SecurityContext的认证流程
面试场景:企业级电商系统的安全认证架构设计
背景
某知名电商平台正在进行核心交易系统的安全升级,面试官张总监正在招聘高级Java后端工程师。今天来面试的是号称"三年经验,五年情怀"的谢飞机,号称精通各种框架,但源码层面还需要..."深度学习"。
第一轮面试:基础概念与架构理解
面试官张总监
"谢飞机你好,我看你简历上说熟悉Spring Security。我们电商平台的用户认证系统正在升级,先问几个基础问题吧。"
问题1:Spring Security的核心架构是什么?它是如何工作的?
谢飞机的回答
"呃,这个嘛...Spring Security就是用来做登录认证的,它有很多过滤器,然后...然后就是配置一下securityConfig,加几个注解@PreAuthorize就能用了。我们项目里就是这么用的,跑得挺好!"
面试官张总监(微笑)
"嗯,你说的对,这是它的基本用法。那你能说说它最核心的组件是什么吗?"
问题2:FilterChainProxy在Spring Security中扮演什么角色?
谢飞机的回答
"FilterChainProxy?哦哦,这个我知道!它就是一个代理,负责管理所有的过滤器。好像是有一个链子,然后把所有的过滤器串起来执行。具体代码嘛..."(挠头)"我看过一点,好像是doFilter方法里有个循环..."
面试官张总监(点头)
"基本概念理解了。那你知道这些过滤器是怎么排序的吗?"
问题3:Spring Security默认的过滤器链有哪些核心过滤器?执行顺序是怎样的?
谢飞机的回答
"默认过滤器?让我想想啊...肯定有个登录的过滤器UsernamePasswordAuthenticationFilter,然后可能还有个Session相关的,嗯...CsrfFilter防跨站攻击的!其他的...其他的我就记不太清了,好像有个BasicAuthenticationFilter用HTTP Basic认证的。顺序嘛...肯定是登录的在前吧?"
面试官张总监
"嗯,理解得还不错。那我们深入一点,聊聊具体的认证流程。"
问题4:SecurityContext是什么?它和Session是什么关系?
谢飞机的回答
"SecurityContext?这个...这个是不是用来存用户信息的?比如登录了之后,就能从里面拿到当前用户?好像有个叫SecurityContextHolder的东西可以获取到。至于和Session的关系...应该是存在Session里的吧?"
面试官张总监(赞许)
"很好!基本概念都清楚了。那你知道它是怎么从Session中存取的吗?"
问题5:SecurityContextPersistenceFilter的作用是什么?
谢飞机的回答
"Persistence...持久化的意思?那就是把SecurityContext保存到...数据库?不对,应该是Session!这个过滤器负责把用户登录后的认证信息保存到Session中,下次请求的时候再从Session里取出来。这个我看过一点,好像是在请求开始的时候从Session里取,请求结束的时候再存回去。"
第二轮面试:认证流程深度解析
面试官张总监
"基础概念掌握得还不错。现在我们深入到具体的认证流程。"
"我们电商平台有千万级用户,用户的认证流程性能和安全性都很重要。"
问题1:当一个用户发起登录请求时,请求是如何在过滤器链中流转的?
谢飞机的回答
"登录请求?比如POST /login?这个请求应该会被UsernamePasswordAuthenticationFilter拦截到,然后它把用户名和密码取出来,封装成一个Token,然后...然后交给AuthenticationManager去认证。认证成功的话,就返回一个Authentication对象,里面包含了用户的信息和权限。这个...这个流程应该差不多吧?"
面试官张总监
"嗯,流程基本正确。那你能具体说说UsernamePasswordAuthenticationFilter的核心代码吗?"
问题2:UsernamePasswordAuthenticationFilter的attemptAuthentication方法是如何工作的?源码是怎么实现的?
谢飞机的回答
"attemptAuthentication...这个方法应该是从request里拿到username和password参数,然后封装成UsernamePasswordAuthenticationToken,最后调用AuthenticationManager的authenticate方法。具体源码...呃...让我想想...大概是:
java
String username = request.getParameter("username");
String password = request.getParameter("password");
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
return getAuthenticationManager().authenticate(token);
大概就是这样,参数名可能不太一样,但逻辑应该差不多。"
面试官张总监(点头)
"很好,逻辑理解得很清楚。那你知道AuthenticationManager是怎么实现认证的吗?"
问题3:AuthenticationManager和ProviderManager的关系是什么?认证逻辑是如何委托给Provider的?
谢飞机的回答
"AuthenticationManager是个接口,ProviderManager是它的实现类。ProviderManager里面应该有一个List,然后遍历这些Provider,挨个调用authenticate方法,如果哪个Provider认证成功了,就返回结果。这个...我记得ProviderManager的源码有个for循环,应该就是这样的逻辑。"
面试官张总监
"理解得很好!那你知道认证成功后的Authentication对象会放到哪里吗?"
问题4:认证成功后,Authentication对象是如何存储到SecurityContext中的?
谢飞机的回答
"这个...认证成功后返回的Authentication对象,应该会被设置到SecurityContext里面。SecurityContext有个setAuthentication方法,应该就是用它来设置。然后SecurityContext会被SecurityContextPersistenceFilter保存到Session中。我看过一点,好像是在FilterChain执行完之后,在finally块里保存的。"
面试官张总监(微笑)
"非常好!那最后一个问题,SecurityContextHolder的实现机制是什么?"
问题5:SecurityContextHolder是如何通过ThreadLocal实现线程隔离的?源码是怎么设计的?
谢飞机的回答
"ThreadLocal!这个我知道!Java里的ThreadLocal可以实现线程隔离,每个线程都有自己的一份副本。SecurityContextHolder内部应该有一个ThreadLocal,然后通过getContext()方法获取当前线程的SecurityContext,setContext()方法设置。这样不同的请求线程之间就不会互相干扰了。源码应该是这样的:
java
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
public static SecurityContext getContext() {
return contextHolder.get();
}
public static void setContext(SecurityContext context) {
contextHolder.set(context);
}
嗯...应该是这样的设计。"
面试官张总监(赞许)
"不错不错!对Spring Security的核心原理理解得很透彻。"
第三轮面试:架构设计与扩展性
面试官张总监
"现在我们聊点更深层次的架构问题。"
"我们电商平台不仅支持用户名密码登录,还支持手机验证码、微信扫码、OAuth2等多种登录方式。这种场景下,Spring Security的架构如何满足需求?"
问题1:如果要支持多种认证方式(如手机验证码、OAuth2、JWT),Spring Security的架构如何扩展?
谢飞机的回答
"多种认证方式?这个...Spring Security应该是可以扩展的吧?我记得有个AuthenticationProvider接口,不同的认证方式可以实现不同的Provider。比如手机验证码就写一个SmsCodeAuthenticationProvider,OAuth2就写一个OAuth2AuthenticationProvider,然后把这些Provider都添加到ProviderManager里面。对了,还需要对应的Filter来拦截不同的登录请求,比如SmsCodeAuthenticationFilter拦截/api/login/sms。这样应该就能支持多种认证方式了!"
面试官张总监(赞许)
"思路很清晰!那你知道如何处理认证成功后的不同业务逻辑吗?"
问题2:不同认证方式认证成功后,可能需要执行不同的业务逻辑(如记录日志、发送消息、更新用户信息),如何设计实现?
谢飞机的回答
"认证成功后的逻辑...我记得有个叫AuthenticationSuccessHandler的接口,可以自定义认证成功后的处理逻辑。每种认证方式可以配置不同的SuccessHandler,比如用户名密码登录成功后跳转到首页,OAuth2登录成功后可能需要绑定用户账号。在Handler里就可以写各种业务逻辑了,比如记录日志、发消息什么的。应该是这样设计的吧?"
面试官张总监
"很好!最后一个问题,关于性能和安全性。"
"电商平台有高并发场景,如何优化Spring Security的性能?同时又要保证安全性?"
问题3:在高并发场景下,Spring Security的性能如何优化?如何平衡安全性和性能?
谢飞机的回答(犹豫)
"性能优化...这个...我想想啊...第一个,可以缓存用户的权限信息,不要每次都查数据库。第二个,可以减少不必要的过滤器,如果有些过滤器用不上就禁用。第三个,Token的话可以用JWT这样无状态的,不用每次都查Session。安全性方面...呃...该有的认证和授权还是要有的,可以在代码层面做优化,比如减少锁竞争,用并发集合什么的...具体的...具体的优化措施我也不是很清楚,可能需要实际测试看看瓶颈在哪里。"
面试官张总监
"嗯,思路是对的。具体优化需要根据实际场景来分析和测试。"
面试结束
面试官张总监
"谢飞机,今天的面试就到这里。总体来说,你对Spring Security的基本概念和核心原理掌握得不错,尤其是对过滤器链和认证流程的理解比较深入。但是在架构设计和性能优化方面,还需要更多的实践经验。"
"你先回去等通知,我们会综合考虑所有候选人。"
谢飞机
"好的好的!谢谢张总监!我回去再好好研究研究源码,希望能有机会加入咱们团队!"
(谢飞机心里默默想:幸好昨天突击了一下,差点就在FilterChainProxy那道题上露馅了...)
详细答案解析
第一轮面试答案
问题1:Spring Security的核心架构
答案:
Spring Security的核心架构是**基于过滤器链(Filter Chain)**的DelegatingFilterProxy设计模式。整个认证授权流程通过一系列的Servlet Filter实现,核心组件包括:
- DelegatingFilterProxy:Spring容器与Servlet容器的桥梁
- FilterChainProxy:Spring Security的核心过滤器,管理SecurityFilterChain
- SecurityFilterChain:包含一组安全过滤器
- SecurityContext:存储认证信息
- AuthenticationManager:认证管理器
- AccessDecisionManager:授权决策器
业务场景: 电商平台用户登录时,请求首先经过DelegatingFilterProxy,然后委托给FilterChainProxy,FilterChainProxy根据请求匹配对应的SecurityFilterChain,链中的各个过滤器依次执行认证授权逻辑。
问题2:FilterChainProxy的角色
核心源码:
java
public class FilterChainProxy extends GenericFilterBean {
private List<SecurityFilterChain> filterChains;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 1. 获取当前请求对应的SecurityFilterChain
List<Filter> filters = getFilters((HttpServletRequest) request);
if (filters == null || filters.size() == 0) {
chain.doFilter(request, response);
return;
}
// 2. 创建VirtualFilterChain执行安全过滤器
VirtualFilterChain vfc = new VirtualFilterChain((HttpServletRequest) request, chain, filters);
vfc.doFilter(request, response);
}
private List<Filter> getFilters(HttpServletRequest request) {
for (SecurityFilterChain chain : filterChains) {
if (chain.matches(request)) {
return chain.getFilters();
}
}
return null;
}
}
业务场景: 电商平台可能有不同的API端点(如/api/user/、/api/order/),FilterChainProxy根据请求路径匹配对应的SecurityFilterChain,不同路径可以使用不同的安全策略。
问题3:默认过滤器链及执行顺序
核心过滤器及顺序:
java
// Spring Security默认的过滤器链(按执行顺序)
1. WebAsyncManagerIntegrationFilter // 异步请求集成
2. SecurityContextPersistenceFilter // 安全上下文持久化(关键!)
3. HeaderWriterFilter // HTTP头写入
4. CsrfFilter // CSRF防护
5. LogoutFilter // 登出处理
6. UsernamePasswordAuthenticationFilter // 用户名密码认证(关键!)
7. DefaultLoginPageGeneratingFilter // 默认登录页
8. DefaultLogoutPageGeneratingFilter // 默认登出页
9. BasicAuthenticationFilter // HTTP Basic认证
10. RequestCacheAwareFilter // 请求缓存
11. SecurityContextHolderAwareRequestFilter // 请求包装
12. AnonymousAuthenticationFilter // 匿名认证
13. SessionManagementFilter // 会话管理
14. ExceptionTranslationFilter // 异常转换(关键!)
15. FilterSecurityInterceptor // 访问控制(关键!)
业务场景: 电商平台用户登录时,SecurityContextPersistenceFilter先从Session中加载用户信息,如果用户已登录,后续过滤器直接使用已认证信息;如果未登录,UsernamePasswordAuthenticationFilter处理登录请求,ExceptionTranslationFilter处理认证异常,FilterSecurityInterceptor进行权限校验。
问题4:SecurityContext与Session的关系
核心概念:
- SecurityContext:Spring Security的认证上下文接口,存储Authentication对象
- SecurityContextRepository:负责SecurityContext的持久化,默认使用HttpSessionSecurityContextRepository
- Session:HTTP会话,用于在多个请求之间存储数据
关系图:
HTTP Session
↓
HttpSessionSecurityContextRepository
↓
SecurityContext (存储Authentication)
↓
SecurityContextHolder (ThreadLocal)
业务场景: 电商用户登录后,Authentication对象(包含用户信息和权限)存储在Session中,后续访问购物车、下单等接口时,从Session中读取用户信息,无需重复登录。
问题5:SecurityContextPersistenceFilter的作用
核心源码:
java
public class SecurityContextPersistenceFilter extends GenericFilterBean {
private SecurityContextRepository repo;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
// 1. 请求开始:从Session中加载SecurityContext
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try {
// 2. 设置到ThreadLocal中,供整个请求链使用
SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.doFilter(req, res);
} finally {
// 3. 请求结束:获取(可能已更新)的SecurityContext
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
// 4. 清空ThreadLocal(重要!防止内存泄漏)
SecurityContextHolder.clearContext();
// 5. 保存到Session中
repo.saveContext(contextAfterChainExecution, req, res);
}
}
}
业务场景: 用户访问订单列表接口时,SecurityContextPersistenceFilter从Session加载用户信息,订单Controller通过SecurityContextHolder获取当前用户,查询该用户的订单数据。
第二轮面试答案
问题1:登录请求在过滤器链中的流转
完整流程图:
HTTP Request: POST /login (username=xxx, password=xxx)
↓
[1] DelegatingFilterProxy (Spring入口)
↓
[2] FilterChainProxy (匹配SecurityFilterChain)
↓
[3] SecurityContextPersistenceFilter (加载Session中的SecurityContext,首次为空)
↓
[4] CsrfFilter (CSRF校验)
↓
[5] UsernamePasswordAuthenticationFilter (拦截/login请求)
│ ↓
│ attemptAuthentication() - 提取用户名密码,封装成Token
│ ↓
│ getAuthenticationManager().authenticate(token)
│ ↓
│ [6] ProviderManager (遍历AuthenticationProvider)
│ ↓
│ [7] DaoAuthenticationProvider (用户名密码认证)
│ ↓
│ loadUserByUsername() - 从数据库加载用户信息
│ ↓
│ passwordEncoder.matches() - 密码校验
│ ↓
│ 返回认证成功的Authentication对象
↓
[8] SecurityContextPersistenceFilter (在finally块中保存到Session)
↓
[9] Session中存储SecurityContext
↓
Response: 登录成功,重定向到首页或返回Token
业务场景: 电商用户输入用户名密码点击登录,请求经过SecurityContextPersistenceFilter(加载Session,首次为空),UsernamePasswordAuthenticationFilter拦截请求,提取认证信息,ProviderManager调用DaoAuthenticationProvider从数据库验证用户,认证成功后,SecurityContextPersistenceFilter将认证信息保存到Session。
问题2:UsernamePasswordAuthenticationFilter核心源码
完整源码:
java
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
// 构造函数:默认拦截POST /login请求
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response)
throws AuthenticationException {
// 1. 从请求中提取用户名和密码
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
// 2. 封装成UsernamePasswordAuthenticationToken(未认证状态)
UsernamePasswordAuthenticationToken authRequest =
new UsernamePasswordAuthenticationToken(username, password);
// 3. 设置请求详细信息(如IP地址、Session ID等)
setDetails(request, authRequest);
// 4. 调用AuthenticationManager进行认证
return this.getAuthenticationManager().authenticate(authRequest);
}
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(SPRING_SECURITY_FORM_USERNAME_KEY);
}
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(SPRING_SECURITY_FORM_PASSWORD_KEY);
}
}
// UsernamePasswordAuthenticationToken构造函数
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal; // 用户名
private Object credentials; // 密码
// 未认证状态的Token(构造函数1)
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false); // 未认证
}
// 已认证状态的Token(构造函数2,认证成功后创建)
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // 已认证
}
}
业务场景: 电商平台登录表单提交用户名和密码,UsernamePasswordAuthenticationFilter提取参数并封装成Token,Authentication对象包含用户身份信息,后续认证流程使用该Token进行验证。
问题3:AuthenticationManager与ProviderManager的关系
核心源码:
java
// AuthenticationManager接口
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
// ProviderManager实现类
public class ProviderManager implements AuthenticationManager, MessageSourceAware {
private List<AuthenticationProvider> providers; // 多个认证提供者
private AuthenticationManager parent; // 父认证管理器
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
// 1. 遍历所有AuthenticationProvider
for (AuthenticationProvider provider : getProviders()) {
// 2. 判断Provider是否支持当前认证类型
if (!provider.supports(toTest)) {
continue;
}
try {
// 3. 调用Provider进行认证
Authentication result = provider.authenticate(authentication);
if (result != null) {
// 4. 认证成功,复制认证详情
copyDetails(authentication, result);
publishAuthenticationSuccess(result);
return result; // 返回认证结果
}
} catch (AccountStatusException e) {
prepareException(e, authentication);
throw e;
} catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}
// 5. 所有Provider都无法认证,尝试父认证管理器
if (parent != null) {
return parent.authenticate(authentication);
}
// 6. 认证失败,抛出异常
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
}
// AuthenticationProvider接口
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication); // 是否支持该认证类型
}
// DaoAuthenticationProvider(用户名密码认证提供者)
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private UserDetailsService userDetailsService; // 用户详情服务
private PasswordEncoder passwordEncoder; // 密码编码器
@Override
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication) {
try {
// 从数据库加载用户信息
UserDetails loadedUser = this.userDetailsService.loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
} catch (UsernameNotFoundException ex) {
throw ex;
} catch (InternalAuthenticationServiceException ex) {
throw ex;
} catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) {
// 1. 校验密码
if (authentication.getCredentials() == null) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
// 2. 使用PasswordEncoder验证密码
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
}
业务场景: 电商平台支持多种认证方式(用户名密码、手机验证码、OAuth2等),ProviderManager管理多个AuthenticationProvider,根据认证类型选择对应的Provider进行认证。
问题4:认证成功后Authentication的存储
存储流程源码:
java
// AbstractAuthenticationProcessingFilter(认证成功处理)
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean {
private AuthenticationSuccessHandler successHandler; // 成功处理器
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult)
throws IOException, ServletException {
// 1. 将认证成功的Authentication存入SecurityContext
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.setContext(context);
// 2. 调用成功处理器(可自定义)
if (successHandler != null) {
successHandler.onAuthenticationSuccess(request, response, authResult);
} else {
// 默认处理
rememberMeServices.loginSuccess(request, response, authResult);
}
}
}
// SecurityContext接口
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication authentication);
}
// SecurityContextImpl实现类
public class SecurityContextImpl implements SecurityContext {
private Authentication authentication;
public SecurityContextImpl() {
}
public SecurityContextImpl(Authentication authentication) {
this.authentication = authentication;
}
@Override
public Authentication getAuthentication() {
return authentication;
}
@Override
public void setAuthentication(Authentication authentication) {
this.authentication = authentication;
}
}
业务场景: 电商用户登录成功后,Authentication对象包含用户ID、用户名、权限列表等信息,存储在SecurityContext中,后续订单服务通过SecurityContextHolder获取当前用户信息。
问题5:SecurityContextHolder的ThreadLocal实现
核心源码:
java
public class SecurityContextHolder {
// 策略模式:通过不同的策略存储SecurityContext
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
public static final String MODE_GLOBAL = "MODE_GLOBAL";
private static String strategyName = MODE_THREADLOCAL;
private static SecurityContextHolderStrategy strategy;
static {
initialize();
}
private static void initialize() {
if (!StringUtils.hasText(strategyName)) {
strategyName = MODE_THREADLOCAL;
}
if (strategyName.equals(MODE_THREADLOCAL)) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
} else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
} else if (strategyName.equals(MODE_GLOBAL)) {
strategy = new GlobalSecurityContextHolderStrategy();
} else {
try {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
}
// ThreadLocalSecurityContextHolderStrategy实现
final class ThreadLocalSecurityContextHolderStrategy
implements SecurityContextHolderStrategy {
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
@Override
public void clearContext() {
contextHolder.remove();
}
@Override
public SecurityContext getContext() {
SecurityContext ctx = contextHolder.get();
if (ctx == null) {
ctx = createEmptyContext();
contextHolder.set(ctx);
}
return ctx;
}
@Override
public void setContext(SecurityContext context) {
contextHolder.set(context);
}
@Override
public SecurityContext createEmptyContext() {
return new SecurityContextImpl();
}
}
// 公开的静态方法
public static void clearContext() {
strategy.clearContext();
}
public static SecurityContext getContext() {
return strategy.getContext();
}
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
}
ThreadLocal原理图:
Thread 1 Thread 2 Thread 3
↓ ↓ ↓
ThreadLocalMap ThreadLocalMap ThreadLocalMap
↓ ↓ ↓
SecurityContext SecurityContext SecurityContext
(User A) (User B) (User C)
业务场景: 电商平台高并发场景下,多个用户同时访问,每个请求对应一个线程,ThreadLocal保证不同用户的认证信息互不干扰,用户A的线程不会访问到用户B的认证信息。
内存泄漏风险: ThreadLocal如果不及时清理,会导致内存泄漏。因此SecurityContextPersistenceFilter在finally块中必须调用clearContext()清理ThreadLocal。
第三轮面试答案
问题1:支持多种认证方式的架构扩展
扩展架构设计:
java
// 1. 自定义认证Token
class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal; // 手机号
private Object credentials; // 验证码
public SmsCodeAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
public SmsCodeAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
}
// 2. 自定义认证Filter
class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/api/login/sms", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) {
String mobile = request.getParameter("mobile");
String code = request.getParameter("code");
SmsCodeAuthenticationToken authRequest =
new SmsCodeAuthenticationToken(mobile, code);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
// 3. 自定义认证Provider
@Component
class SmsCodeAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public Authentication authenticate(Authentication authentication) {
SmsCodeAuthenticationToken token = (SmsCodeAuthenticationToken) authentication;
String mobile = (String) token.getPrincipal();
String code = (String) token.getCredentials();
// 1. 校验验证码
String savedCode = redisTemplate.opsForValue().get("sms:code:" + mobile);
if (savedCode == null || !savedCode.equals(code)) {
throw new BadCredentialsException("验证码错误");
}
// 2. 加载用户信息
UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
// 3. 返回认证成功的Token
return new SmsCodeAuthenticationToken(userDetails, code,
userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}
// 4. 配置SecurityConfig
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SmsCodeAuthenticationProvider smsCodeAuthenticationProvider;
@Autowired
private SmsCodeAuthenticationFilter smsCodeAuthenticationFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) {
// 添加自定义Provider
auth.authenticationProvider(smsCodeAuthenticationProvider);
// 默认的DaoAuthenticationProvider
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/login/**", "/api/send-sms").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
// 添加自定义Filter
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.csrf().disable();
}
}
架构图:
FilterChainProxy
↓
├─ UsernamePasswordAuthenticationFilter → DaoAuthenticationProvider
├─ SmsCodeAuthenticationFilter → SmsCodeAuthenticationProvider
├─ OAuth2LoginAuthenticationFilter → OAuth2LoginAuthenticationProvider
└─ JwtAuthenticationFilter → JwtAuthenticationProvider
业务场景: 电商平台支持用户名密码登录、手机验证码登录、微信扫码登录等多种方式,用户可以根据偏好选择登录方式,提升用户体验。
问题2:认证成功后的业务逻辑处理
SuccessHandler实现:
java
// 自定义认证成功处理器
@Component
public class CustomAuthenticationSuccessHandler
implements AuthenticationSuccessHandler {
@Autowired
private UserService userService;
@Autowired
private LogService logService;
@Autowired
private MessageService messageService;
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException, ServletException {
// 1. 获取当前用户
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String username = userDetails.getUsername();
// 2. 更新用户最后登录时间
userService.updateLastLoginTime(username, new Date());
// 3. 记录登录日志
logService.recordLoginLog(username, request.getRemoteAddr(),
new Date(), "登录成功");
// 4. 发送欢迎消息
User user = userService.getByUsername(username);
if (user.getEmail() != null) {
messageService.sendWelcomeEmail(user.getEmail(), user.getNickname());
}
// 5. 生成JWT Token
String token = JwtUtil.generateToken(userDetails);
// 6. 返回响应
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("message", "登录成功");
result.put("token", token);
result.put("userInfo", user);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(result));
}
}
// 配置SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomAuthenticationSuccessHandler successHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginProcessingUrl("/api/login")
.successHandler(successHandler) // 自定义成功处理器
.and()
// ...其他配置
}
}
// 不同认证方式可以使用不同的SuccessHandler
@Component
public class SmsCodeAuthenticationSuccessHandler
implements AuthenticationSuccessHandler {
// 手机验证码登录成功后的特殊处理
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) {
// 1. 清除Redis中的验证码
String mobile = request.getParameter("mobile");
redisTemplate.delete("sms:code:" + mobile);
// 2. 检查是否是新用户,首次登录需要绑定用户名密码
// ...
}
}
业务场景: 电商用户登录成功后,系统自动记录登录日志(用于安全审计)、更新最后登录时间(用于安全提醒)、发送欢迎邮件(提升用户体验)、生成JWT Token(用于后续API调用认证)。
问题3:高并发场景下的性能优化
性能优化方案:
1. 减少不必要的过滤器
java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 禁用不必要的过滤器
.csrf().disable() // 如果使用JWT,可以禁用CSRF
.sessionManagement().disable() // 无状态认证,禁用Session管理
.requestCache().disable() // 不需要请求缓存
.securityContext().disable() // 无状态,不需要SecurityContext
.anonymous().disable() // 不需要匿名认证
// ...其他配置
}
}
2. 使用缓存优化
java
// 自定义UserDetailsService,使用Redis缓存用户信息
@Service
public class CachedUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private PasswordEncoder passwordEncoder;
private static final String USER_CACHE_PREFIX = "user:cache:";
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
String cacheKey = USER_CACHE_PREFIX + username;
// 1. 先从缓存获取
UserDetails cachedUser = (UserDetails) redisTemplate.opsForValue().get(cacheKey);
if (cachedUser != null) {
return cachedUser;
}
// 2. 缓存未命中,从数据库查询
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
// 3. 构建UserDetails
UserDetails userDetails = User.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles(user.getRoles().toArray(new String[0]))
.build();
// 4. 写入缓存,过期时间30分钟
redisTemplate.opsForValue().set(cacheKey, userDetails, 30, TimeUnit.MINUTES);
return userDetails;
}
}
3. 使用JWT无状态认证
java
// JWT认证Filter,不依赖Session
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
// 1. 从请求头获取Token
String token = request.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
// 2. 解析Token,验证签名
token = token.substring(7);
Claims claims = JwtUtil.parseToken(token);
if (claims == null) {
chain.doFilter(request, response);
return;
}
// 3. 从Token中获取用户信息
String username = claims.getSubject();
List<String> roles = (List<String>) claims.get("roles");
// 4. 构建Authentication对象,存入SecurityContext
List<GrantedAuthority> authorities = roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(username, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
// 5. 继续过滤器链
chain.doFilter(request, response);
}
}
4. 异步处理认证成功后的业务逻辑
java
@Component
public class AsyncAuthenticationSuccessHandler
implements AuthenticationSuccessHandler {
@Autowired
private AsyncTaskExecutor taskExecutor;
@Autowired
private LogService logService;
@Autowired
private MessageService messageService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) {
// 1. 主线程快速返回响应
String token = JwtUtil.generateToken(authentication);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":200,\"message\":\"登录成功\",\"token\":\"" + token + "\"}");
// 2. 异步执行耗时操作(日志、邮件等)
taskExecutor.execute(() -> {
try {
String username = authentication.getName();
logService.recordLoginLog(username, request.getRemoteAddr(), new Date());
messageService.sendWelcomeEmail(username);
} catch (Exception e) {
log.error("登录成功后处理失败", e);
}
});
}
}
5. 权限信息缓存
java
// 自定义FilterSecurityInterceptor,缓存权限决策结果
public class CachedFilterSecurityInterceptor extends FilterSecurityInterceptor {
@Autowired
private RedisTemplate<String, Boolean> redisTemplate;
private static final String AUTH_CACHE_PREFIX = "auth:cache:";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String username = SecurityContextHolder.getContext().getAuthentication().getName();
String requestURI = httpRequest.getRequestURI();
String method = httpRequest.getMethod();
// 1. 构建缓存Key
String cacheKey = AUTH_CACHE_PREFIX + username + ":" + method + ":" + requestURI;
// 2. 从缓存获取权限决策结果
Boolean cachedDecision = redisTemplate.opsForValue().get(cacheKey);
if (cachedDecision != null && cachedDecision) {
chain.doFilter(request, response);
return;
}
// 3. 缓存未命中,执行权限校验
super.doFilter(request, response, chain);
// 4. 缓存决策结果(假设校验通过)
redisTemplate.opsForValue().set(cacheKey, true, 5, TimeUnit.MINUTES);
}
}
性能优化对比:
| 优化措施 | 优化前 | 优化后 | 提升效果 | |----------------------|----------------|----------------|----------------| | 禁用不必要的过滤器 | 15个过滤器 | 8个过滤器 | 减少约40%开销 | | 用户信息缓存 | 每次查询数据库 | Redis缓存 | QPS提升10倍 | | JWT无状态认证 | Session集群同步 | 无状态 | 减少网络开销 | | 异步处理日志邮件 | 阻塞主线程 | 异步执行 | 响应时间减少80% | | 权限决策缓存 | 每次校验 | 缓存决策结果 | 减少重复计算 |
业务场景: 电商平台双十一大促,每秒数十万次登录请求,通过以上优化措施,将登录接口的平均响应时间从500ms降低到50ms,系统QPS从2000提升到20000。
总结
本文通过面试官张总监与程序员谢飞机的对话,深入解析了Spring Security认证流程的核心源码,包括:
- FilterChainProxy:过滤器链的核心协调者
- SecurityContextPersistenceFilter:安全上下文的持久化管理
- UsernamePasswordAuthenticationFilter:用户名密码认证的核心过滤器
- ProviderManager:认证管理器的委托实现
- SecurityContextHolder:基于ThreadLocal的安全上下文持有者
- 多种认证方式扩展:通过自定义Filter和Provider支持多种认证
- 性能优化:缓存、异步、无状态认证等优化策略
Spring Security的设计遵循了单一职责原则 、开闭原则 、依赖倒置原则 等设计原则,通过过滤器链模式 、策略模式 、模板方法模式等设计模式,实现了高度可扩展的安全认证架构。
核心技术要点:
- 掌握Spring Security的核心过滤器链机制
- 理解SecurityContext的生命周期管理
- 掌握AuthenticationManager的委托机制
- 理解ThreadLocal在多线程场景下的应用
- 掌握如何扩展Spring Security支持多种认证方式
- 了解高并发场景下的性能优化策略
希望本文能帮助读者深入理解Spring Security的核心原理,在实际项目中灵活运用!
参考文档
- Spring Security官方文档:https://docs.spring.io/spring-security/reference/
- Spring Security源码:https://github.com/spring-projects/spring-security
- Spring Security认证流程详解
- 企业级应用安全架构设计最佳实践
本文首发于CSDN,转载请注明出处