You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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查询数据库获取用户信息。执行流程:

  1. 登录应用获取Bearer token;
  2. 调用/impersonate?username=anotherUser;
  3. TokenAuthenticationFilter解析token更新安全上下文;
  4. SwitchUserFilter完成用户切换更新上下文;
  5. 重定向到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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 05:05:20