🚀 作者主页: 有来技术
🔥 开源项目: youlai-mall 🍃 vue3-element-admin 🍃 youlai-boot
🌺 仓库主页: Gitee 💫 Github 💫 GitCode
💖 欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请纠正!
目录
- 问题描述
- 问题分析
- 解决方案
- 源码解析
- ExceptionTranslationFilter#doFilter
- DispatcherServlet#doDispatch
- DispatcherServlet#processHandlerException
- ExceptionHandlerExceptionResolver#doResolveHandlerMethodException
- 结语
- 参考
问题描述
在基于 Spring Boot 3 + Spring Security 6 的权限管理系统开源项目 youlai-boot 中,根据官方提供的思路重写 AccessDeniedHandler
实现自定义异常处理,但发现该实现并没有生效,而是被全局异常捕获。期望的响应是 {"code":"A0301","msg":"访问未授权"}
,但实际上获得的响应与期望的不符,具体响应如下图所示:
关键代码如下:
-
自定义异常处理器
MyAccessDeniedHandler
package com.youlai.system.core.security.exception; /** * Spring Security 访问异常处理器 * * @author haoxr * @since 2022/10/18 */ @Component public class MyAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_UNAUTHORIZED); } }
-
Spring Security 配置 SecurityConfig
package com.youlai.system.core.security.config; /** * Spring Security 权限配置 * * @author haoxr * @since 2023/2/17 */ @Configuration @EnableWebSecurity @EnableMethodSecurity @RequiredArgsConstructor public class SecurityConfig { private final MyAuthenticationEntryPoint authenticationEntryPoint; private final MyAccessDeniedHandler accessDeniedHandler; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http // ... .exceptionHandling(httpSecurityExceptionHandlingConfigurer -> httpSecurityExceptionHandlingConfigurer .authenticationEntryPoint(authenticationEntryPoint) // 未认证异常处理 .accessDeniedHandler(accessDeniedHandler) // 未授权访问异常处理 ) // ... ; return http.build(); } }
-
全局异常处理器
package com.youlai.system.common.exception; /** * 全局系统异常处理器 **/ @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { // ... // 捕获 Exception @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public <T> Result<T> handleException(Exception e) { log.error("unknown exception: {}", e.getMessage()); return Result.failed(e.getLocalizedMessage()); } }
-
测试接口
在删除角色接口中,使用了 Spring Security 提供的权限控制注解
@PreAuthorize
来进行权限校验。使用该注解可以在方法级别上对用户的权限进行校验,只有具有相应权限的用户才能执行该方法,否则会提示用户无权访问。@Operation(summary = "删除角色") @DeleteMapping("/{ids}") @PreAuthorize("@ss.hasPerm('sys:role:delete')") public Result deleteRoles( @Parameter(description ="删除角色,多个以英文逗号(,)分割") @PathVariable String ids ) { boolean result = roleService.deleteRoles(ids); return Result.judge(result); }
问题分析
现象:当存在全局异常处理器时,自定义的 Spring Security 权限异常拦截处理器失效;而当移除全局异常处理器时,自定义的权限异常拦截处理器将正常生效。
猜测:全局异常处理器会干扰到 Spring Security 的权限异常处理流程,导致自定义的处理器无法按预期执行。
根据猜测,直接定位 Spring Security 异常处理和转换的过滤器 ExceptionTranslationFilter
的 doFilter
方法。
通过分析代码逻辑,可以确定问题的根源在于无权限访问异常被全局异常处理器捕获并处理掉了,因此不会进入 catch 分支执行 handleSpringSecurityException
方法。具体执行过程放在下文的源码解析章节。
解决方案
为了确保异常能够传递给 Spring Security 的自定义异常处理器,你可以对全局异常处理器(GlobalExceptionHandler
)进行修改。如果异常是 AuthenticationException
或 AccessDeniedException
,则继续将其抛出以便交给自定义处理器处理。
package com.youlai.system.common.exception;
//...
/**
* 全局系统异常处理器
**/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public <T> Result<T> handleException(Exception e) throws Exception{
// 将 Spring Security 异常继续抛出,以便交给自定义处理器处理
if (e instanceof AccessDeniedException
|| e instanceof AuthenticationException) {
throw e;
}
log.error("unknown exception: {}", e.getMessage());
return Result.failed(e.getLocalizedMessage());
}
}
通过这样的修改,当遇到 AuthenticationException
或者 AccessDeniedException
异常时,它们将被继续抛出,从而能够进入到 Spring Security 的自定义异常处理器进行处理。其他类型的异常仍然会在当前异常处理逻辑中进行处理。
修改后,按照自定义异常处理的输出,测试正常。
源码解析
接下来通过源码阅读分析:为什么在存在全局异常处理器的情况下,Spring Security的无权限访问异常无法被自定义异常处理器处理?
ExceptionTranslationFilter#doFilter
在没有全局异常处理器的情况下,异常没有被拦截处理,会进入 catch
分支,由自定义异常处理器处理。
但如果存在全局异常处理器,则不会抛出异常,也就不会进入 catch
分支。
因此,需要进一步探究 chain.doFilter(request, response)
的后续处理,以了解为什么在有全局异常处理器的情况下没有异常抛出。
DispatcherServlet#doDispatch
DispatcherServlet#processHandlerException
DispatcherServlet#processDispatchResult
调用 DispatcherServlet#processHandlerException
方法处理异常
有无全局异常处理器
在这个方法可以体现出区别了。
-
有全局异常处理器
-
无全局异常处理器
有全局异常处理器 得到 exMv
不为 null ,后续不抛出异常,所以需要在 exMv = resolver.resolveException(request, response, handler, ex);
断点看下里面的逻辑。
ExceptionHandlerExceptionResolver#doResolveHandlerMethodException
调用栈 HandlerExceptionResolverComposite#resolveException → AbstractHandlerExceptionResolver#resolveException → ExceptionHandlerExceptionResolver#doResolveHandlerMethodException
-
有全局异常处理器
-
无全局异常处理器
结语
在对异常处理流程进行详细的源码解读后,我们不仅解决了Spring Security权限异常处理在存在全局异常处理器时失效的问题,还对Spring MVC的异常处理机制有了更深入的了解。这一探索不仅有助于更好地理解框架的内部机制,也为未来的项目调优提供了有益的经验。
参考
- https://stackoverflow.com/questions/31074040/custom-accessdeniedhandler-not-called
- https://github.com/spring-projects/spring-security/issues/6908