Spring SwitchUserFilter重定向后丢失用户信息问题排查
我在结合SwitchUserFilter与自定义Token认证机制时遇到问题,期望通过高权限用户模拟现有用户。使用Spring Security提供的SwitchUserFilter适配项目后未达预期,配置及执行流程如下:
安全配置代码
public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomUserDetailsService customUserDetailsService; @Bean public TokenAuthenticationFilter tokenAuthenticationFilter() { return new TokenAuthenticationFilter(); } @Bean public SwitchUserFilter switchUserFilter() { var filter = new SwitchUserFilter(); filter.setUserDetailsService(customUserDetailsService); filter.setSwitchUserUrl("/impersonate"); filter.setSwitchFailureUrl("/switchUser"); filter.setTargetUrl("/user"); // 该接口已在应用中实现(GET /user) return filter; } @Override protected void configure(HttpSecurity http) throws Exception { // 简化后的配置,仅保留复现问题的核心逻辑 // 实际项目中还配置了端点权限控制等其他安全规则 http.csrf().disable(); http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); http.addFilterAfter(switchUserFilter(), FilterSecurityInterceptor.class); } }
Token认证过滤器实现
public class TokenAuthenticationFilter extends OncePerRequestFilter { @Autowired private TokenProvider tokenProvider; @Autowired private CustomUserDetailsService customUserDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String jwt = getJwtFromRequest(request); if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { Long userId = tokenProvider.getUserIdFromToken(jwt); UserDetails userDetails = customUserDetailsService.loadUserById(userId); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception ex) { log.error("Could not set user authentication in security context", ex); } filterChain.doFilter(request, response); } }
执行流程及问题
token中存储用户ID,CustomUserDetailsService通过ID查询数据库获取用户信息。执行流程:
- 登录应用获取Bearer token;
- 调用
/impersonate?username=anotherUser; TokenAuthenticationFilter解析token更新安全上下文;SwitchUserFilter完成用户切换更新上下文;- 重定向到
GET /user时,TokenAuthenticationFilter再次解析token恢复原用户,导致请求使用原用户而非切换后的用户。
请问该组合机制应如何正确工作?我哪里配置或流程有误?
核心问题出在过滤器执行顺序和Token认证过滤器的逻辑缺陷上,以下是具体修复步骤:
1. 调整SwitchUserFilter的执行顺序
当前把SwitchUserFilter放在FilterSecurityInterceptor之后是错误的。Spring Security过滤器链中,认证类过滤器需要在权限校验类之前执行。正确做法是将SwitchUserFilter放在TokenAuthenticationFilter之后、FilterSecurityInterceptor之前,确保用户切换操作在权限校验前完成,且能覆盖Token认证的上下文。
修改configure(HttpSecurity http)方法:
@Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable(); // 先执行Token认证 http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); // 将SwitchUserFilter放在Token认证之后,权限拦截之前 http.addFilterAfter(switchUserFilter(), TokenAuthenticationFilter.class); }
2. 修改TokenAuthenticationFilter逻辑,跳过已切换用户的认证
SwitchUserFilter完成切换后,会在SecurityContext中放入带有SwitchUserFilter.ROLE_PREVIOUS_ADMINISTRATOR权限的认证对象。Token过滤器需要检测这个特殊权限,若存在则跳过Token解析,保留当前切换后的用户上下文。
更新TokenAuthenticationFilter的doFilterInternal方法:
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { // 检查当前是否为切换后的用户,若是则跳过Token认证 Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication(); if (existingAuth != null && existingAuth.getAuthorities().stream() .anyMatch(auth -> auth.getAuthority().equals(SwitchUserFilter.ROLE_PREVIOUS_ADMINISTRATOR))) { filterChain.doFilter(request, response); return; } String jwt = getJwtFromRequest(request); if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { Long userId = tokenProvider.getUserIdFromToken(jwt); UserDetails userDetails = customUserDetailsService.loadUserById(userId); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception ex) { log.error("Could not set user authentication in security context", ex); } filterChain.doFilter(request, response); }
3. 限制切换操作的发起权限
给发起切换的用户添加ROLE_ADMIN或自定义权限,并配置接口访问限制:
@Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable(); http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); http.addFilterAfter(switchUserFilter(), TokenAuthenticationFilter.class); // 仅允许管理员发起用户切换 http.authorizeRequests() .antMatchers("/impersonate").hasRole("ADMIN") .anyRequest().authenticated(); }
原理说明
SwitchUserFilter执行时,会将原用户信息存入SecurityContext,并创建包含切换后用户信息和ROLE_PREVIOUS_ADMINISTRATOR权限的认证对象。- 调整过滤器顺序后,切换操作在Token认证之后执行,确保切换后的上下文覆盖Token解析结果。
- Token过滤器检测特殊权限,避免后续请求中Token解析覆盖已切换的用户上下文。
内容的提问来源于stack exchange,提问作者Marius Manastireanu

