使用@Preauthorize和@@ ControllerAdvice的自定义错误消息

Har*_*rsh 7 annotations spring-mvc spring-security

我们正在使用spring-spring-security-3.2.最近我们将@PreAuthorize注释添加到RestAPIs(之前它是基于URL的).

     @PreAuthorize("hasPermission('salesorder','ViewSalesOrder')")
  @RequestMapping(value = "/restapi/salesorders/", method = RequestMethod.GET)
  public ModelAndView getSalesOrders(){}
Run Code Online (Sandbox Code Playgroud)

我们已经有了使用@AtrollerAdvice和自定义PermissionEvaluator注释的全局异常处理程序,除错误消息外,一切正常.

让我们说有些用户在没有"ViewSalesOrder"权限的情况下访问API.默认情况下,spring会抛出异常"访问被拒绝",但没有告诉哪个权限丢失(我们要求提及缺少哪个权限).

是否可以抛出一个也包含权限名称的异常,因此最终的错误消息应该看起来像"访问被拒绝,你需要ViewSalesOrder权限"(这里的权限名称应该来自@PreAuthorize注释)?

请注意,我们有100个这样的restAPI,因此非常感谢通用解决方案.

Mer*_* Z. 5

没有很好的方法可以实现您的期望,因为PermissionEvaluator 界面不允许您将缺少的权限与评估结果一起传递。
此外,AccessDecisionManager决定关于AccessDecisionVoter实例投票的最终授权,其中之一是PreInvocationAuthorizationAdviceVoter关于@PreAuthorize价值评估的投票。

长话短说,PreInvocationAuthorizationAdviceVoter对请求票(给请求-1点),当您的自定义PermissionEvaluator返回falsehasPermission调用。如您所见,无法在此流中传播失败的原因。

On the other hand, you may try some workarounds to achieve what you want.

One way can be to throw an exception within your custom PermissionEvaluator when permission check fails. You can use this exception to propagate the missing permission to your global exception handler. There, you can pass the missing permission to your message descriptors as a parameter. Beware that this will halt execution process of AccessDecisionManager which means successive voters will not be executed (defaults are RoleVoter and AuthenticatedVoter). You should be careful if you choose to go down this path.

Another safer but clumsier way can be to implement a custom AccessDeniedHandler and customize the error message before responding with 403. AccessDeniedHandler为您提供当前HttpServletRequest可用于检索请求 URI。但是,在这种情况下的坏消息是,您需要一个 URI 到权限映射,以便定位丢失的权限。


小智 5

我已经实现了 Mert Z 提到的第二种可能的解决方案。我的解决方案仅适用于 API 层中使用的 @PreAuthorize 注释(例如使用 @RequestMapping)。我已经注册了一个自定义 AccessDeniedHandler bean,在其中获取禁止的 API 方法的 @PreAuthorize 注释的值并将其填充到错误消息中。

public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    private DispatcherServlet dispatcherServlet;

    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException,
            ServletException {
        if (!response.isCommitted()) {
            List<HandlerMapping> handlerMappings = dispatcherServlet.getHandlerMappings();
            if (handlerMappings != null) {
                HandlerExecutionChain handler = null;
                for (HandlerMapping handlerMapping : handlerMappings) {
                    try {
                        handler = handlerMapping.getHandler(request);
                    } catch (Exception e) {}
                    if (handler != null)
                        break;
                }
                if (handler != null && handler.getHandler() instanceof HandlerMethod) {
                    HandlerMethod method = (HandlerMethod) handler.getHandler();
                    PreAuthorize methodAnnotation = method.getMethodAnnotation(PreAuthorize.class);
                    if (methodAnnotation != null) {
                        response.sendError(HttpStatus.FORBIDDEN.value(),
                                "Authorization condition not met: " + methodAnnotation.value());
                        return;
                    }
                }
            }
            response.sendError(HttpStatus.FORBIDDEN.value(),
                    HttpStatus.FORBIDDEN.getReasonPhrase());
        }
    }

    @Inject
    public void setDispatcherServlet(DispatcherServlet dispatcherServlet) {
        this.dispatcherServlet = dispatcherServlet;
    }
}
Run Code Online (Sandbox Code Playgroud)

该处理程序在 WebSecurityConfigurerAdapter 中注册:

@EnableGlobalMethodSecurity(jsr250Enabled = true, prePostEnabled = true)
@EnableWebSecurity
public abstract class BaseSecurityInitializer extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ...
        http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());
        ...
    }

    @Bean
    public AccessDeniedHandler accessDeniedHandler() {
        return new CustomAccessDeniedHandler();
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,如果还有一个带有 @ControllerAdvice 的全局资源异常处理程序,则 CustomAccessDeniedHandler 将不会被执行。我通过在全局处理程序中重新抛出异常解决了这个问题(按照此处的建议https://github.com/spring-projects/spring-security/issues/6908):

@ControllerAdvice
public class ResourceExceptionHandler {
    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity accessDeniedException(AccessDeniedException e) throws AccessDeniedException {
        log.info(e.toString());
        throw e;
    }
}
Run Code Online (Sandbox Code Playgroud)