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

Spring Security认证成功后异常重定向问题排查与方案咨询

问题分析与解决方案

问题现象

为API端点添加访问控制时,无论请求/api/apple还是/api/orange,服务都会重定向到/。当前通过继承AbstractAuthenticationProcessingFilter实现自定义头认证,但该过滤器设计为认证成功后重定向,不符合API无状态、返回原请求响应的需求。

原因分析

AbstractAuthenticationProcessingFilter是为**交互式认证场景(如表单登录)**设计的,其默认逻辑中,认证成功后会调用successfulAuthentication方法,该方法会自动重定向到配置的successUrl(未配置时默认跳转到/)。这种重定向行为完全不适用于API接口,因为API需要保持请求上下文,直接返回业务响应而非跳转页面。

此外,你在过滤器中手动设置SecurityContextHolder.getContext().setAuthentication(authentication),这会干扰Spring Security的认证流程,导致后续逻辑混乱。

正确实现方案

对于基于自定义请求头的预认证场景,应该使用Spring Security提供的AbstractPreAuthenticatedProcessingFilter——它专门为预认证场景(如基于请求头、客户端证书的认证)设计,默认不会触发重定向,完全适配API的无状态认证需求。

步骤1:修改过滤器实现

将SessionAuthFilter改为继承AbstractPreAuthenticatedProcessingFilter,重写获取预认证主体和凭证的方法:

public class SessionAuthFilter extends AbstractPreAuthenticatedProcessingFilter {
    private final CustomUserDetails userDetails;

    protected SessionAuthFilter(RequestMatcher requestMatcher, AuthenticationManager authenticationManager,
                                CustomUserDetails userDetails) {
        super(requestMatcher, authenticationManager);
        this.userDetails = userDetails;
        // 设置认证失败时抛出异常,避免后续过滤器误处理
        setThrowExceptionWhenTokenRejected(true);
    }

    @Override
    protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
        String sessionToken = request.getHeader("SessionToken");
        return sessionToken != null ? sessionToken.trim() : null;
    }

    @Override
    protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
        // 预认证场景下,凭证可设为固定值,后续由Provider验证有效性
        return "N/A";
    }
}

步骤2:调整认证提供者逻辑

修改PreAuthProvider,确保认证失败时抛出明确异常,同时正确验证token对应的用户有效性:

@Component
@Slf4j
public class PreAuthProvider implements AuthenticationProvider {

    private final CustomUserDetails userDetails;

    // 注入CustomUserDetails用于验证token有效性
    public PreAuthProvider(CustomUserDetails userDetails) {
        this.userDetails = userDetails;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        if (!supports(authentication.getClass())) {
            return null;
        }

        String sessionToken = (String) authentication.getPrincipal();
        log.debug("PreAuthenticated authentication request: {}", authentication);

        // 验证token对应的用户是否存在且有效
        UserDetails user;
        try {
            user = userDetails.loadUserByUsername(sessionToken);
        } catch (UsernameNotFoundException e) {
            throw new BadCredentialsException("Invalid SessionToken");
        }

        if (!user.isCredentialsNonExpired()) {
            throw new InsufficientAuthenticationException("SessionToken has expired");
        }

        // 返回已认证的Authentication对象
        return new PreAuthenticatedAuthenticationToken(user.getUsername(), user.getPassword(), user.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return PreAuthenticatedAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

步骤3:修正SecurityFilterChain配置

确保过滤器添加到正确位置,同时保持无状态会话配置:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    AuthenticationManager customAuthenticationManager(PreAuthProvider preAuthProvider) {
        return new ProviderManager(List.of(preAuthProvider));
    }

    @Bean
    SessionAuthFilter customAuthFilter(AuthenticationManager authManager, CustomUserDetails userDetails) {
        SessionAuthFilter filter = new SessionAuthFilter(
                new OrRequestMatcher(
                        new AntPathRequestMatcher("/apple/**"),
                        new AntPathRequestMatcher("/orange/**")
                ),
                authManager,
                userDetails);
        // 自定义认证成功逻辑:不重定向,直接继续请求链
        filter.setAuthenticationSuccessHandler((request, response, auth) -> {});
        return filter;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, SessionAuthFilter authFilter) throws Exception {
        http.exceptionHandling()
                .authenticationEntryPoint(new Http403ForbiddenEntryPoint())
                .accessDeniedHandler(new AccessDeniedHandlerImpl())
                .and()
                .formLogin().disable()
                .httpBasic().disable()
                .authorizeRequests()
                .antMatchers(
                        "/",
                        "/error",
                        "/v3/api-docs/**",
                        "/swagger-ui/**",
                        "/swagger-ui.html",
                        "/actuator/**"
                ).permitAll()
                .antMatchers(HttpMethod.GET, "/apple").hasAuthority("getApples")
                .antMatchers(HttpMethod.GET, "/orange").hasAuthority("getOranges")
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(authFilter, AbstractPreAuthenticatedProcessingFilter.class)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        return http.build();
    }
}

关键优化点

  1. 替换过滤器类型:用AbstractPreAuthenticatedProcessingFilter替代AbstractAuthenticationProcessingFilter,彻底避免默认重定向行为。
  2. 单一职责拆分:过滤器只负责提取认证信息,用户验证逻辑交给AuthenticationProvider,符合Spring Security的设计规范。
  3. 明确异常处理:设置throwExceptionWhenTokenRejected(true),确保认证失败时直接返回403/401,而非继续执行请求链。
  4. 自定义成功处理器:覆盖认证成功后的逻辑,不做任何跳转,让请求直接流向业务控制器。

内容的提问来源于stack exchange,提问作者superflux

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 06:18:24