如何配置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
相关产品推荐
相关产品推荐

