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

如何避免AccessDeniedHandler执行后触发AuthenticationEntryPoint?

问题解决:Spring Security 6.2.3 中 JWT 认证与权限异常的正确状态码返回

核心问题分析

你的问题根源在于Spring Security 异常处理逻辑的执行顺序冲突,以及过滤器中异常抛出/处理方式不正确,导致:

  1. 权限不足(AccessDeniedException)时,accessDeniedHandler执行后,authenticationEntryPoint被二次触发,最终返回401而非403
  2. 无效JWT(AuthenticationException)时,authenticationEntryPoint被重复调用

以下是针对性的解决方案:


1. 修正 JwtRequestFilter 异常抛出逻辑

确保过滤器仅在JWT无效/验证失败时抛出AuthenticationException,且不手动处理响应,完全交给Spring Security的异常处理机制:

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    private final JwtTokenUtil jwtTokenUtil;
    private final UserDetailsService userDetailsService;

    public JwtRequestFilter(JwtTokenUtil jwtTokenUtil, UserDetailsService userDetailsService) {
        this.jwtTokenUtil = jwtTokenUtil;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        final String authorizationHeader = request.getHeader("Authorization");

        String username = null;
        String jwt = null;

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwt = authorizationHeader.substring(7);
            try {
                username = jwtTokenUtil.extractUsername(jwt);
            } catch (JwtException e) {
                // 无效JWT直接抛出AuthenticationException,不手动处理响应
                throw new BadCredentialsException("Invalid JWT token", e);
            }
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
            if (!jwtTokenUtil.validateToken(jwt, userDetails)) {
                throw new BadCredentialsException("Invalid JWT token");
            }
            // 验证通过,设置认证信息到上下文
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }
}

2. 正确配置 SecurityFilterChain 异常处理

在Spring Security 6.x中,通过exceptionHandling()明确划分authenticationEntryPoint(处理未认证/无效JWT)和accessDeniedHandler(处理权限不足)的职责,确保配置顺序正确,且无重复处理逻辑:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(jsr250Enabled = true) // 启用@RolesAllowed注解支持
public class WebSecurityConfig {

    private final JwtRequestFilter jwtRequestFilter;

    public WebSecurityConfig(JwtRequestFilter jwtRequestFilter) {
        this.jwtRequestFilter = jwtRequestFilter;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/public/**").permitAll()
                        .anyRequest().authenticated()
                )
                .exceptionHandling(exceptions -> exceptions
                        // 处理未认证、无效JWT场景,返回401
                        .authenticationEntryPoint(unauthorizedEntryPoint())
                        // 处理权限不足(@RolesAllowed校验失败)场景,返回403
                        .accessDeniedHandler(accessDeniedHandler())
                )
                .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    // 自定义401响应处理器
    @Bean
    public AuthenticationEntryPoint unauthorizedEntryPoint() {
        return (request, response, ex) -> {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json");
            response.getWriter().write("{\"error\":\"Unauthorized\",\"message\":\"" + ex.getMessage() + "\"}");
        };
    }

    // 自定义403响应处理器
    @Bean
    public AccessDeniedHandler accessDeniedHandler() {
        return (request, response, ex) -> {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            response.setContentType("application/json");
            response.getWriter().write("{\"error\":\"Forbidden\",\"message\":\"" + ex.getMessage() + "\"}");
        };
    }
}

3. 移除冲突的异常处理逻辑

如果保留了@ControllerAdvice作为临时方案,需确保它不处理Spring Security相关异常,避免与Security内置异常处理器冲突:

@ControllerAdvice
public class GlobalExceptionHandler {

    // 仅处理业务异常,排除AuthenticationException和AccessDeniedException
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException ex) {
        return new ResponseEntity<>(new ErrorResponse(ex.getMessage()), HttpStatus.BAD_REQUEST);
    }
}

关键排查点

  • 检查JwtRequestFilter中是否存在手动调用response.sendError()或写入响应的代码,这类操作会触发后续异常处理器重复执行
  • 确认@EnableMethodSecurity的jsr250Enabled属性为true,否则@RolesAllowed注解不会生效
  • 避免同时配置多个异常处理机制(如Security内置处理器+@ControllerAdvice处理Security异常)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 11:03:19