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

Spring Security仅返回401 Unauthorized问题排查诉求

问题:自定义异常无法返回正确HTTP状态码,始终返回401 Unauthorized

我开发了一个基于Spring Boot 3的REST API,用Spring Security + JWT做认证。现在遇到个问题:触发自定义异常(比如RecipeNotFoundException应该返回404)或者服务器内部错误时,始终返回401 Unauthorized,而不是对应的状态码。


相关代码实现

1. SecurityFilterChain 配置

@Bean
protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    // No need for CSRF in this example
    http
        .cors()
            .and()
        .csrf().disable()
        // No authentication
        .authorizeHttpRequests()
            .requestMatchers("/login", "/register").permitAll()
            // Authentication for other requests
            .anyRequest().authenticated()
            .and()
        // Session is stateless
        .exceptionHandling()
            .authenticationEntryPoint(jwtAuthenticationEntryPoint)
            .and()
        .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
        .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    return http.build();
}

2. JwtAuthenticationEntryPoint 实现

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
}

3. 自定义异常类及业务使用

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class RecipeNotFoundException extends RuntimeException {
}

业务逻辑中触发异常:

if (recipeFound.isEmpty()) throw new RecipeNotFoundException();

4. JwtRequestFilter 实现

@Component
public class JwtRequestFilter extends OncePerRequestFilter {
    private final JwtUserDetailsService jwtUserDetailsService;
    private final JwtTokenUtil jwtTokenUtil;

    public JwtRequestFilter(JwtUserDetailsService jwtUserDetailsService, JwtTokenUtil jwtTokenUtil) {
        this.jwtUserDetailsService = jwtUserDetailsService;
        this.jwtTokenUtil = jwtTokenUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        if (!request.getRequestURI().equals("/register") && !request.getRequestURI().equals("/login")) {
            final String requestTokenHeader = request.getHeader("Authorization");

            String username = null;
            String jwtToken = null;

            // Remove Bearer word from token
            if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
                jwtToken = requestTokenHeader.substring(7);
                try {
                    username = jwtTokenUtil.getUsernameFromToken(jwtToken);
                } catch (IllegalArgumentException e) {
                    System.out.println("Unable to get JWT Token");
                } catch (ExpiredJwtException e) {
                    System.out.println("JWT Token has expired");
                }
            } else {
                logger.warn("JWT Token does not start with Bearer String");
            }

            // Validate token
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username);

                // Configure authentication in Spring Security
                if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
                    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                            new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                    // Successful Authentication
                    SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                }
            }
        }
        chain.doFilter(request, response);
    }
}

问题原因分析

  1. 异常处理覆盖问题:你只配置了AuthenticationEntryPoint,它仅处理认证相关异常(比如JWT无效、未携带Token),但自定义业务异常、服务器内部错误不属于认证异常。不过因为你设置了.anyRequest().authenticated(),如果JWT过滤器没有在SecurityContext中设置合法的Authentication,Spring Security会直接判定请求未认证,触发AuthenticationEntryPoint返回401,覆盖后续的异常响应。

  2. JWT过滤器逻辑缺陷:当JWT无效/过期时,你仅打印日志但未明确触发认证失败,也没有终止请求流程。此时SecurityContext为空,后续走到授权环节时,Spring Security会直接返回401,导致业务异常根本没机会被处理。


解决方案

步骤1:添加全局异常处理器

创建全局异常处理类,专门捕获业务异常和通用异常:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(RecipeNotFoundException.class)
    public ResponseEntity<String> handleRecipeNotFound(RecipeNotFoundException ex) {
        return new ResponseEntity<>("食谱不存在", HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGeneralException(Exception ex) {
        return new ResponseEntity<>("服务器内部错误", HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

步骤2:修复JWT过滤器的认证失败逻辑

在JWT无效/过期时,抛出Spring Security标准认证异常,明确触发认证失败流程:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
    if (!request.getRequestURI().equals("/register") && !request.getRequestURI().equals("/login")) {
        final String requestTokenHeader = request.getHeader("Authorization");

        String username = null;
        String jwtToken = null;

        // 处理Token格式
        if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
            jwtToken = requestTokenHeader.substring(7);
            try {
                username = jwtTokenUtil.getUsernameFromToken(jwtToken);
            } catch (IllegalArgumentException e) {
                logger.error("无法解析JWT Token", e);
                throw new BadCredentialsException("无效的JWT Token");
            } catch (ExpiredJwtException e) {
                logger.error("JWT Token已过期", e);
                throw new CredentialsExpiredException("JWT Token已过期");
            }
        } else if (requestTokenHeader != null) {
            logger.warn("JWT Token格式错误,未以Bearer开头");
            throw new BadCredentialsException("JWT Token格式无效");
        } else {
            // 未携带Token,直接放行到授权环节触发401
            chain.doFilter(request, response);
            return;
        }

        // 验证Token并设置认证信息
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username);

            if (!jwtTokenUtil.validateToken(jwtToken, userDetails)) {
                throw new BadCredentialsException("无效的JWT Token");
            }

            UsernamePasswordAuthenticationToken authToken =
                    new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

            SecurityContextHolder.getContext().setAuthentication(authToken);
        }
    }
    chain.doFilter(request, response);
}

步骤3:完善SecurityFilterChain的异常处理

添加accessDeniedHandler处理授权失败场景,确保认证通过后的异常能被全局处理器捕获:

@Bean
protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .cors().and()
        .csrf().disable()
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/login", "/register").permitAll()
            .anyRequest().authenticated()
        )
        .exceptionHandling(exceptions -> exceptions
            .authenticationEntryPoint(jwtAuthenticationEntryPoint)
            .accessDeniedHandler((request, response, ex) -> {
                response.sendError(HttpServletResponse.SC_FORBIDDEN, "权限不足");
            })
        )
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        )
        .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    return http.build();
}

关键说明

  • 全局异常处理器负责处理认证通过后的业务异常和通用错误,返回对应状态码。
  • JWT过滤器在认证失败时抛出标准异常,由AuthenticationEntryPoint处理返回401。
  • 只有当请求成功通过认证(SecurityContext中有合法Authentication),业务逻辑抛出的异常才会被全局处理器捕获,返回正确的响应。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 02:50:34