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

如何配置Spring Security在认证失败时返回401而非403状态码

问题根因

状态码被覆盖、自定义失败处理器不生效的核心原因有两点:

  • 你通过addFilter()手动实例化并注册的JwtAuthenticationFilter不会被Spring自动注入容器中的Bean,你不手动给它设置失败处理器,它永远会使用内部默认初始化的SimpleUrlAuthenticationFailureHandler,这也是你之前自定义AuthenticationFailureHandler从未触发的根本原因。
  • 默认的SimpleUrlAuthenticationFailureHandler调用sendError(401)时,并不会立刻把响应返回给客户端,只是把401状态暂存在请求属性中,请求会继续走完后续过滤器链。后续的ExceptionTranslationFilter捕获到残留的认证异常时,由于你配置了无状态会话、又没有显式配置认证入口点,会默认将认证异常包装为访问拒绝异常,最终把响应状态覆盖为403。
解决方案

第一步:编写独立的认证失败处理器

不要依赖默认实现,在处理器中直接写入401状态并提交响应,阻止后续逻辑篡改状态码:

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

public class JwtAuthFailureHandler implements AuthenticationFailureHandler {
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
        // 重置响应,清除之前写入的临时状态
        response.reset();
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        // 可按需自定义错误响应结构
        Map<String, Object> errorResp = Map.of(
                "code", HttpStatus.UNAUTHORIZED.value(),
                "message", "认证失败:凭证无效",
                "data", null
        );
        response.getWriter().write(OBJECT_MAPPER.writeValueAsString(errorResp));
        // 关键:提交响应,终止请求继续向后传递
        response.flushBuffer();
    }
}

第二步:修改安全配置,手动绑定处理器+配置兜底异常处理

手动给自定义过滤器注入失败处理器,同时配置全局异常兜底逻辑,彻底避免状态码被覆盖:

@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true, jsr250Enabled = true)
public class SecurityConfiguration {

    @Autowired
    private AuthenticationConfiguration authenticationConfiguration;

    @Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception {
        AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();
        JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager);
        // 手动给过滤器绑定自定义失败处理器
        jwtAuthenticationFilter.setAuthenticationFailureHandler(new JwtAuthFailureHandler());

        JwtAuthorisationFilter jwtAuthorisationFilter = new JwtAuthorisationFilter();

        http.cors().and().csrf().disable()
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                // 明确指定过滤器顺序,避免顺序混乱导致的异常
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .addFilterAfter(jwtAuthorisationFilter, BasicAuthenticationFilter.class)
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 配置兜底异常处理,严格区分401和403语义
                .exceptionHandling()
                // 认证失败兜底:未携带凭证、凭证无效场景返回401
                .authenticationEntryPoint((request, response, authException) -> {
                    response.reset();
                    response.setStatus(HttpStatus.UNAUTHORIZED.value());
                    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                    Map<String, Object> errorResp = Map.of(
                            "code", HttpStatus.UNAUTHORIZED.value(),
                            "message", "请携带有效认证凭证访问",
                            "data", null
                    );
                    response.getWriter().write(OBJECT_MAPPER.writeValueAsString(errorResp));
                    response.flushBuffer();
                })
                // 权限不足兜底:已认证但无权限访问场景返回403,不混淆语义
                .accessDeniedHandler((request, response, accessDeniedException) -> {
                    response.reset();
                    response.setStatus(HttpStatus.FORBIDDEN.value());
                    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                    Map<String, Object> errorResp = Map.of(
                            "code", HttpStatus.FORBIDDEN.value(),
                            "message", "无权限访问该资源",
                            "data", null
                    );
                    response.getWriter().write(OBJECT_MAPPER.writeValueAsString(errorResp));
                    response.flushBuffer();
                });

        return http.build();
    }
}

第三步:检查JwtAuthenticationFilter的实现逻辑

确认过滤器捕获到BadCredentialsException等认证异常时,直接调用绑定的失败处理器处理,不要将异常继续抛给后续过滤器链,也不要在失败处理逻辑执行后调用filterChain.doFilter()放行请求。如果你的过滤器继承自AbstractAuthenticationProcessingFilter,只需要确保重写的unsuccessfulAuthentication方法正确调用失败处理器即可,父类默认实现不会继续向后传递请求。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 14:30:44