Spring Security 6.3 权限异常处理实战解析

news/2024/12/27 16:50:57/

一、问题描述

在开发员工管理系统(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() : "操作失败");}
}

http://www.ppmy.cn/news/1557913.html

相关文章

Spark-Streaming receiver模式源码解析

一、上下文 《Spark-Streaming初识》博客中我们用NetworkWordCount例子大致了解了Spark-Streaming receiver模式的运行。下面我们就通过该代码进行源码分析,深入了解其原理。 二、构建StreamingContext 它是Spark Streaming功能的主要入口点。并提供了从各种输入…

12.19问答解析

概述 某中小型企业有四个部门,分别是市场部、行政部、研发部和工程部,请合理规划IP地址和VLAN,实现企业内部能够互联互通,同时要求市场部、行政部和工程部能够访问外网环境(要求使用OSPF协议),研发部不能访问外网环境…

微服务篇-深入了解 MinIO 文件服务器(你还在使用阿里云 0SS 对象存储图片服务?教你使用 MinIO 文件服务器:实现从部署到具体使用)

🔥博客主页: 【小扳_-CSDN博客】 ❤感谢大家点赞👍收藏⭐评论✍ 文章目录 1.0 MinIO 文件服务器概述 1.1 MinIO 使用 Docker 部署 1.2 MinIO 控制台的使用 2.0 使用 Java 操作 MinIO 3.0 使用 minioClient 对象的方法 3.1 判断桶是否存在 3.2…

将三个list往一个excel表的三个sheet中写入,能用多线程提高写入速度

1. 多线程大批量写入可能导致 OOM 多线程可以加速写入操作,因为每个线程可以独立处理一个 Sheet。 但多线程会导致内存占用增加,因为多个线程可能同时将数据加载到内存中。 如果每个 List 数据量过大,而 JVM 的堆内存不够,就会触…

DALL-M:基于大语言模型的上下文感知临床数据增强方法 ,补充

DALL-M:基于大语言模型的上下文感知临床数据增强方法 ,补充 论文大纲理解结构分析数据分析1. 数据收集2. 数据处理和规律挖掘3. 相关性分析4. 数学模型建立解法拆解1. 逻辑关系拆解子解法拆解: 2. 逻辑链分析3. 隐性方法分析4. 隐性特征分析5…

软件测试丨性能测试工具-JMeter

JMeter的基本功能概述 JMeter是一个开源的性能测试工具,它带有图形用户界面(GUI),专为负载测试和性能测试而设计。我们可以利用它来模拟多个用户同时访问应用程序,以判断其在不同负载下的表现。更令人兴奋的是&#x…

机器学习中数据预处理的方法

数据预处理是机器学习项目中至关重要的一步,它直接影响模型的性能和准确性。 一、数据清洗 数据清洗是数据预处理的首要步骤,主要目的是处理数据中的缺失值、异常值和重复数据等。 1.处理缺失值: 删除含有缺失值的行或列。 均值填充&#…

webserver log日志系统的实现

参考博客:https://blog.csdn.net/weixin_51322383/article/details/130474753 https://zhuanlan.zhihu.com/p/721880618 阻塞队列blockqueue 1、阻塞队列的设计流程是什么样的 它的底层是用deque进行管理的 阻塞队列主要是围绕着生产者消费者模式进行多线程的同步和…