You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 12:47:51