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

Spring Boot 3集成Spring Security误拦截415异常返回403问题求助

问题分析

你的问题核心是Spring Security集成后,非认证类异常(如415媒体类型不支持)被错误拦截返回403,根源在于:

  1. Spring Security的过滤器链执行顺序早于Spring MVC的内容类型校验逻辑,导致部分请求在进入MVC处理前就被Security拦截;
  2. 默认的Security异常处理未区分「认证失败」和「业务/请求格式异常」,统一返回了403。
解决方案

1. 添加全局异常处理器,确保415异常正常返回

通过@ControllerAdvice定义全局异常处理,专门捕获HttpMediaTypeNotSupportedException并返回415状态码,优先级高于Security的异常处理:

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
    public ResponseEntity<Map<String, String>> handleMediaTypeNotSupported(HttpMediaTypeNotSupportedException ex) {
        Map<String, String> error = new HashMap<>();
        error.put("status", String.valueOf(HttpStatus.UNSUPPORTED_MEDIA_TYPE.value()));
        error.put("message", "不支持的媒体类型:" + ex.getContentType());
        error.put("supportedTypes", ex.getSupportedMediaTypes().toString());
        return new ResponseEntity<>(error, HttpStatus.UNSUPPORTED_MEDIA_TYPE);
    }
}

2. 配置Spring Security,仅对认证/权限异常返回403

修改SecurityFilterChain配置,自定义异常处理逻辑,只在未认证或权限不足时返回403,其他异常交给Spring MVC处理:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
               .cors().and()
               .csrf().disable()
               .authorizeHttpRequests(auth -> auth
                   .requestMatchers("/auth/**").permitAll() // 改为/auth/**更规范,匹配所有子路径
                   .anyRequest().authenticated())
               .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
               .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
               .userDetailsService(jpaUserDetailsService)
               // 自定义异常处理
               .exceptionHandling(exceptions -> exceptions
                   .authenticationEntryPoint((request, response, authException) -> {
                       // 仅未认证时返回403
                       response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                       response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                       response.getWriter().write("{\"status\":403,\"message\":\"未授权访问\"}");
                   })
                   .accessDeniedHandler((request, response, accessDeniedException) -> {
                       // 权限不足时返回403
                       response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                       response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                       response.getWriter().write("{\"status\":403,\"message\":\"权限不足\"}");
                   })
               )
               .build();
}

3. 优化JwtAuthFilter逻辑

确保token无效时不干扰SecurityContext,让permitAll的接口能正常进入MVC层处理:

@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
    private final JwtUtils jwtUtils;
    private static final String AUTHENTICATION_HEADER = "Authorization";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String jwtToken = request.getHeader(AUTHENTICATION_HEADER);

        if (jwtToken != null && jwtToken.startsWith("Bearer ")) {
            jwtToken = jwtToken.split(" ")[1];
            if (jwtUtils.isValidToken(jwtToken)) {
                UserDetails userDetails = new UserSecurity(jwtUtils.extractUser(jwtToken));
                UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
            // token无效时不做任何操作,保持SecurityContext为空即可
        }

        filterChain.doFilter(request, response);
    }
}

可选:调整Controller的consumes配置

如果GET请求不需要强制application/json,可以去掉类上的consumes属性,仅对POST等需要接收JSON的方法单独配置:

@RestController
@RequestMapping(value = "auth", produces = MediaType.APPLICATION_JSON_VALUE)
public class AuthController {
    @GetMapping
    public Object test() {
        Map<String, String> test = new HashMap<>();
        test.put("key", "val");
        return test;
    }

    // 仅POST请求要求Content-Type为application/json
    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
    public Object login(@RequestBody LoginRequest request) {
        // 登录逻辑
        return null;
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 20:05:31