Spring Security 6未认证与未授权场景异常消息区分问题
解决方案:区分未认证与未授权的自定义响应
核心问题分析
- 过滤器链优先级高于方法注解:当设置
anyRequest().authenticated()时,Spring Security过滤器链会先于@PreAuthorize/@Secured等方法注解执行权限检查。已认证但无权限的请求会被过滤器链拦截,抛出InsufficientAuthenticationException,而非方法注解抛出的AccessDeniedException,导致两种场景无法区分。 - 缺少未授权异常处理器:仅配置
AuthenticationEntryPoint处理未认证的AuthenticationException,但未配置专门的处理器处理已登录但无权限的AccessDeniedException。
步骤1:自定义AccessDeniedHandler处理未授权场景
创建专门处理器,针对已登录但无权限的请求返回403 Forbidden自定义响应:
@Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { private static final Logger logger = LoggerFactory.getLogger(CustomAccessDeniedHandler.class); @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { logger.error("Access denied error: {}", accessDeniedException.getMessage()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setStatus(HttpServletResponse.SC_FORBIDDEN); final Map<String, Object> body = new HashMap<>(); body.put("status", HttpServletResponse.SC_FORBIDDEN); body.put("error", "Access Denied"); body.put("message", "你没有访问该资源的权限"); body.put("path", request.getRequestURI()); final ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(response.getOutputStream(), body); } }
步骤2:调整AuthenticationEntryPoint返回未认证标准响应
修改原处理器,将未认证场景的状态码改为HTTP标准的401 Unauthorized,与403场景明确区分:
@Component public class AuthEntryPoint implements AuthenticationEntryPoint { private static final Logger logger = LoggerFactory.getLogger(AuthEntryPoint.class); @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { logger.error("Unauthenticated error: {}", authException.getMessage()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); final Map<String, Object> body = new HashMap<>(); body.put("status", HttpServletResponse.SC_UNAUTHORIZED); body.put("error", "Unauthenticated"); body.put("message", "请先登录以访问该资源"); body.put("path", request.getRequestURI()); final ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(response.getOutputStream(), body); } }
步骤3:更新SecurityConfig,同时配置两个异常处理器
在过滤器链中同时注册两个异常处理器,并移除与方法注解冲突的权限规则:
@Configuration @EnableMethodSecurity(securedEnabled = true, prePostEnabled = true) public class SecurityConfig { @Autowired private AuthEntryPoint unauthorizedHandler; @Autowired private CustomAccessDeniedHandler accessDeniedHandler; // 省略authenticationProvider、authenticationJwtTokenFilter等Bean定义 @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeHttpRequests() .requestMatchers("/", "/register", "/login").permitAll() // 移除过滤器链内的权限规则,交由方法注解处理 .anyRequest().authenticated() .and().sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and().httpBasic() .and().authenticationProvider(authenticationProvider()) .exceptionHandling() .authenticationEntryPoint(unauthorizedHandler) // 处理未认证 .accessDeniedHandler(accessDeniedHandler) // 处理未授权 .and() .addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }
步骤4:用方法注解实现细粒度权限控制
在Controller方法上添加注解,定义权限要求:
@RestController public class TestController { @GetMapping("/test2") @PreAuthorize("hasAuthority('ROLE_USER')") public String test2() { return "Hello, ROLE_USER!"; } }
效果验证
- 未认证访问:返回401状态码,响应消息为"请先登录以访问该资源"
- 已登录但无权限访问:返回403状态码,响应消息为"你没有访问该资源的权限"
内容的提问来源于stack exchange,提问作者Janas
相关产品推荐
相关产品推荐

