Spring Boot 3中自定义AccessDeniedException提示缺失权限
我正在使用Spring Boot 3.0.6(对应Spring Security 6.0.3),现有一个标注@PreAuthorize并指定权限的接口:
@RestController @RequestMapping(path = "/perfil") public class ProfileController { @GetMapping({ "/all" }) @PreAuthorize("hasAuthority('ROLE_ROL_PROFILE_ADMINISTRATION')") public ResponseEntity<List<Profile>> findAll() { List<Profile> list = profileService.findAllProfiles(); return new ResponseEntity<List<Profile>>(list, HttpStatus.OK); } }
同时我有一个实现ErrorController的@RestControllerAdvice类,用于处理通用错误:
@RestControllerAdvice public class BaseExceptionHandler implements ErrorController { @ExceptionHandler(AccessDeniedException.class) public ResponseEntity<HttpResponse> accessDeniedException(AccessDeniedException ex) { LOGGER.error(ex.getMessage()); String msg = StringUtils.isBlank(ex.getMessage()) ? NOT_ENOUGH_PERMISSIONS : ex.getMessage(); return crearHttpResponse (HttpStatus.FORBIDDEN, msg); } @ExceptionHandler(HttpRequestMethodNotSupportedException.class) public ResponseEntity<HttpResponse> methodNotSupportedtException(HttpRequestMethodNotSupportedException e) { HttpMethod methodAllowed = Objects.requireNonNull(e.getSupportedHttpMethods()).iterator().next(); return crearHttpResponse (HttpStatus.METHOD_NOT_ALLOWED, String.format(METHOD_NOT_ALLOWED, methodAllowed)); } }
我可以通过HttpRequestMethodNotSupportedException的API自定义返回提示,但希望对AccessDeniedException实现同样效果,告知用户缺失的角色权限。查看源码发现AuthorizationManagerBeforeMethodInterceptor仅抛出提示为“Access Denied”的异常,无法直接获取缺失权限,想了解Spring Security 6下的最优实现方式。
方案一:自定义AuthorizationManager,抛出携带缺失权限的异常
Spring Security 6中@PreAuthorize的权限校验由AuthorizationManager驱动,我们可以自定义包装类增强原有实现,在校验失败时抛出包含缺失权限的自定义异常,是方法级权限校验场景的最优方案。
1. 定义自定义异常类
public class AccessDeniedWithMissingAuthorityException extends AccessDeniedException { private final String requiredAuthority; public AccessDeniedWithMissingAuthorityException(String requiredAuthority) { super("缺失必要权限: " + requiredAuthority); this.requiredAuthority = requiredAuthority; } public String getRequiredAuthority() { return requiredAuthority; } }
2. 实现自定义MethodAuthorizationManager
@Component public class CustomMethodAuthorizationManager implements MethodAuthorizationManager { private final MethodAuthorizationManager delegate; public CustomMethodAuthorizationManager(MethodAuthorizationManager delegate) { this.delegate = delegate; } @Override public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation methodInvocation) { AuthorizationDecision decision = this.delegate.check(authentication, methodInvocation); if (!decision.isGranted()) { // 从方法上的@PreAuthorize注解提取权限规则 PreAuthorize preAuthorize = AnnotationUtils.findAnnotation(methodInvocation.getMethod(), PreAuthorize.class); if (preAuthorize != null) { String expression = preAuthorize.value(); // 解析hasAuthority('XXX')格式的表达式,提取权限值 Pattern pattern = Pattern.compile("hasAuthority\\('(.*?)'\\)"); Matcher matcher = pattern.matcher(expression); if (matcher.find()) { String requiredAuthority = matcher.group(1); throw new AccessDeniedWithMissingAuthorityException(requiredAuthority); } } // 兜底抛出默认异常 throw new AccessDeniedException("Access Denied"); } return decision; } }
3. 替换默认的AuthorizationManager
在Spring Security配置类中,替换方法授权管理器:
@Configuration @EnableMethodSecurity public class SecurityConfig { @Bean public MethodAuthorizationManager methodAuthorizationManager(MethodSecurityExpressionHandler expressionHandler) { DefaultMethodAuthorizationManager defaultManager = new DefaultMethodAuthorizationManager(); defaultManager.setExpressionHandler(expressionHandler); return new CustomMethodAuthorizationManager(defaultManager); } }
4. 修改异常处理器处理自定义异常
@RestControllerAdvice public class BaseExceptionHandler implements ErrorController { // 原有异常处理方法... @ExceptionHandler(AccessDeniedWithMissingAuthorityException.class) public ResponseEntity<HttpResponse> accessDeniedWithMissingAuthorityException(AccessDeniedWithMissingAuthorityException ex) { LOGGER.error(ex.getMessage()); return crearHttpResponse(HttpStatus.FORBIDDEN, String.format("您缺少必要权限:%s", ex.getRequiredAuthority())); } }
方案二:基于AuthorizationEvent监听的无侵入式实现
如果不想修改权限校验逻辑,可以通过监听Spring Security的授权事件,捕获缺失权限信息后传递给异常处理器,适合需要低侵入的场景。
1. 实现授权拒绝事件监听器
@Component public class AuthorizationDeniedEventListener implements ApplicationListener<AuthorizationDeniedEvent> { private final ThreadLocal<String> requiredAuthorityHolder = new ThreadLocal<>(); @Override public void onApplicationEvent(AuthorizationDeniedEvent event) { if (event.getAuthorizationContext() instanceof MethodInvocationAuthorizationContext methodContext) { PreAuthorize preAuthorize = AnnotationUtils.findAnnotation(methodContext.getMethodInvocation().getMethod(), PreAuthorize.class); if (preAuthorize != null) { String expression = preAuthorize.value(); Pattern pattern = Pattern.compile("hasAuthority\\('(.*?)'\\)"); Matcher matcher = pattern.matcher(expression); if (matcher.find()) { requiredAuthorityHolder.set(matcher.group(1)); } } } } public String getRequiredAuthority() { return requiredAuthorityHolder.get(); } public void clear() { requiredAuthorityHolder.remove(); } }
2. 修改异常处理器获取权限信息
@RestControllerAdvice public class BaseExceptionHandler implements ErrorController { @Autowired private AuthorizationDeniedEventListener authorizationListener; @ExceptionHandler(AccessDeniedException.class) public ResponseEntity<HttpResponse> accessDeniedException(AccessDeniedException ex) { LOGGER.error(ex.getMessage()); String requiredAuthority = authorizationListener.getRequiredAuthority(); String msg; if (requiredAuthority != null) { msg = String.format("您缺少必要权限:%s", requiredAuthority); authorizationListener.clear(); } else { msg = StringUtils.isBlank(ex.getMessage()) ? NOT_ENOUGH_PERMISSIONS : ex.getMessage(); } return crearHttpResponse(HttpStatus.FORBIDDEN, msg); } // 原有异常处理方法... }
优先选择方案一,它直接针对@PreAuthorize方法级校验场景,实现逻辑清晰,能够精准提取所需权限并抛出带信息的异常,后续异常处理逻辑简单直接,完全契合Spring Security 6的架构设计。如果是混合URL权限与方法权限的场景,可以结合方案一和自定义AccessDeniedHandler统一返回格式。
内容的提问来源于stack exchange,提问作者user2087103

