一、问题描述
在开发员工管理系统(EMS)时,遇到一个权限异常处理的问题:
2024-12-24T20:33:42.891+08:00 ERROR 17144 --- [ems] [nio-8080-exec-2]
com.ems.handler.GlobalExceptionHandler : 全局异常信息:Access Denied
明明在 SecurityConfig 中配置了 CustomerAccessDeniedHandler
来处理权限不足异常,但实际运行时异常却被 GlobalExceptionHandler
捕获并处理了(日志显示:“全局异常信息:Access Denied”),导致自定义的权限处理器失效。
二、核心代码分析
1. 权限不足处理器
@Component
@Slf4j
public class CustomerAccessDeniedHandler implements AccessDeniedHandler{@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {log.error("权限不足,URI:{},异常:{}", request.getRequestURI(), accessDeniedException.getMessage());// 发生这个行为,做响应处理,给一个响应的结果response.setContentType("application/json;charset=utf-8");// 构建输出流对象ServletOutputStream outputStream = response.getOutputStream();// 调用fastjson工具,进行Result对象序列化String error = JSON.toJSONString(Result.error("权限不足,请联系管理员"));outputStream.write(error.getBytes(StandardCharsets.UTF_8));outputStream.flush();outputStream.close();}
}
2. Security 配置类
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity //开启SpringSecurity的自定义配置(在SpringBoot项目中可以省略)
@EnableMethodSecurity // 开启方法级安全注解
public class SecurityConfig {// 自定义的用于认证的过滤器,进行jwt的校验操作private final JwtTokenOncePerRequestFilter jwtTokenFilter;// 认证用户无权限访问资源的处理器private final CustomerAccessDeniedHandler customerAccessDeniedHandler;// 客户端进行认证数据的提交时出现异常,或者是匿名用户访问受限资源的处理器private final AnonymousAuthenticationHandler anonymousAuthentication;// 用户认证校验失败处理器private final LoginFailureHandler loginFailureHandler;/*** 创建BCryptPasswordEncoder注入容器,用于密码加密*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 登录时调用AuthenticationManager.authenticate执行一次校验*/@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {return config.getAuthenticationManager();}@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception{// 添加自定义异常处理类http.exceptionHandling(configurer -> {configurer.accessDeniedHandler(customerAccessDeniedHandler) // 配置认证用户无权限访问资源的处理器.authenticationEntryPoint(anonymousAuthentication); // 配置匿名用户未认证的处理器});// 配置关闭csrf机制http.csrf(AbstractHttpConfigurer::disable);// 用户认证校验失败处理器http.formLogin(conf -> conf.failureHandler(loginFailureHandler));// STATELESS(无状态):表示应用程序是无状态的,不创建会话http.sessionManagement(conf -> conf.sessionCreationPolicy(SessionCreationPolicy.STATELESS));// 配置放行路径http.authorizeHttpRequests(auth -> auth.requestMatchers("/swagger-ui/**","/swagger-ui.html","/swagger-resources/**","/v3/api-docs/**","/webjars/**","/doc.html","/emp/login" // 修改登录接口路径).permitAll().anyRequest().authenticated());// 配置过滤器的执行顺序http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);return http.build();}}
3. JWT认证过滤器
// 每一个servlet请求,只执行一次
@Component
@Slf4j
public class JwtTokenOncePerRequestFilter extends OncePerRequestFilter {@Autowiredprivate JwtProperties jwtProperties;@Autowiredprivate LoginFailureHandler loginFailureHandler;// 添加白名单路径列表private final String[] whitelist = {"/emp/login","/swagger-ui/**","/swagger-ui.html","/swagger-resources/**","/v3/api-docs/**","/webjars/**","/doc.html"};@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 1. 判断当前请求是否在白名单中String uri = request.getRequestURI();if (isWhitelisted(uri)) {filterChain.doFilter(request, response);return;}try {this.validateToken(request);} catch (AuthenticationException e) {loginFailureHandler.onAuthenticationFailure(request, response, e);return;}filterChain.doFilter(request, response);}// 判断请求路径是否在白名单中private boolean isWhitelisted(String uri) {for (String pattern : whitelist) {if (pattern.endsWith("/**")) {// 处理通配符路径String basePattern = pattern.substring(0, pattern.length() - 3);if (uri.startsWith(basePattern)) {return true;}} else if (pattern.equals(uri)) {// 精确匹配return true;}}return false;}private void validateToken(HttpServletRequest request) {// 说明:登录了,再次请求其他需要认证的资源String token = request.getHeader("Authorization");if (ObjectUtils.isEmpty(token)) { // header没有tokentoken = request.getParameter("Authorization");}if (ObjectUtils.isEmpty(token)) {throw new CustomerAuthenticationException("token为空");}// 校验tokenEmpLogin empLogin;try {log.info("jwt校验:{}", token);Claims claims = JwtUtil.parseJWT(jwtProperties.getSecretKey(), token);String loginUserString = claims.get(JwtClaimsConstant.EMP_LOGIN).toString();// 把json字符串转为对象empLogin = JSON.parseObject(loginUserString, EmpLogin.class);log.info("当前员工id:{}", empLogin.getEmp().getId());BaseContext.setCurrentId(empLogin.getEmp().getId());} catch (Exception ex) {throw new CustomerAuthenticationException("token校验失败");}// 把校验后的用户信息再次放入到SpringSecurity的上下文中UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(empLogin, null,empLogin.getAuthorities()); // 已认证的 Authentication 对象,包含用户的权限信息SecurityContextHolder.getContext().setAuthentication(authentication);System.out.println(empLogin.getAuthorities());}
}
三、问题原因
问题出在 Spring Security 的异常处理流程上:
Security过滤器链 ---> DispatcherServlet ---> Controller ----> AOP权限校验 ----> 全局异常处理
Spring Security 的权限校验机制
Spring Security 提供了两种权限校验方式:
- URL级别校验(配置式):在过滤器链中进行
// 1. URL级别的权限检查(配置式)
http.authorizeHttpRequests(auth -> auth.requestMatchers("/admin/**").hasRole("ADMIN").anyRequest().authenticated()
);
- 方法级别校验(注解式):(@PreAuthorize)通过 AOP 实现
@PreAuthorize("hasAuthority('ems:news.select')")
@GetMapping("/getNews")
public String getNews() {return "news列表";
}
问题根源
在这里我使用的是方法级别的权限检查,方法级的权限注解(@PreAuthorize)是通过 AOP 实现的,AOP 抛出的异常会被 Spring MVC 的异常处理机制捕获,因此@RestControllerAdvice 的优先级高于 Security 的异常处理器
三、解决方案
修改全局异常处理器
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {@ExceptionHandler(Exception.class)public Result ex(Exception ex){// 权限异常转交给Security处理if(ex instanceof AccessDeniedException) {log.info("捕获到权限异常,转交给Security处理");throw (AccessDeniedException)ex;}log.error("全局异常信息:{}", ex.getMessage());return Result.error(StringUtils.hasLength(ex.getMessage()) ? ex.getMessage() : "操作失败");}
}