@Preauthorize和@@ ControllerAdvice

时间:2017-07-12 14:07:55

标签: spring-mvc spring-security annotations

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

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

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

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

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

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

2 个答案:

答案 0 :(得分:2)

由于{ 'cinema': 1, 'fire_station': 1, 'library': 1, 'railway_station': 1, 'school': 2, 'supermarket': 1 } [ {'field': 'bus_station', 'choice': 'Bus Station', 'count': 0}, {'field': 'car_park', 'choice': 'Car Park', 'count': 0}, {'field': 'cinema', 'choice': 'Cinema', 'count': 1}, {'field': 'dentist', 'choice': 'Dentist', 'count': 0}, {'field': 'doctor', 'choice': 'Doctor', 'count': 0}, {'field': 'fire_station', 'choice': 'Fire Station', 'count': 1}, {'field': 'garden', 'choice': 'Public Garden', 'count': 0}, {'field': 'hospital', 'choice': 'Hospital', 'count': 0}, {'field': 'leisure_centre', 'choice': 'Leisure Centre', 'count': 0}, {'field': 'library', 'choice': 'Library', 'count': 1}, {'field': 'public_service', 'choice': 'Public Service', 'count': 0}, {'field': 'railway_station', 'choice': 'Railway Station', 'count': 1}, {'field': 'school', 'choice': 'School', 'count': 2}, {'field': 'supermarket', 'choice': 'Supermarket', 'count': 1}, {'field': 'woodland', 'choice': 'Woodland', 'count': 0} ] 界面不允许您将遗漏的权限与评估结果一起传递,因此没有很好的方法可以实现您的期望。
此外,PermissionEvaluator决定对AccessDecisionManager个实体的投票的最终授权,其中一个是AccessDecisionVoter,其对PreInvocationAuthorizationAdviceVoter值的评估进行投票。

长话短说,当您的自定义@PreAuthorize返回PreInvocationAuthorizationAdviceVoterPermissionEvaluator时,false投票反对请求(给出请求-1点)。如您所见,无法在此流程中传播失败的原因。

另一方面,您可以尝试一些变通方法来实现您的目标。

一种方法是在权限检查失败时在您的自定义hasPermission内抛出异常。您可以使用此异常将缺少的权限传播到全局异常处理程序。在那里,您可以将缺少的权限作为参数传递给消息描述符。请注意,这将暂停PermissionEvaluator的执行过程,这意味着不会执行连续的选民(默认为RoleVoterAuthenticatedVoter)。如果你选择走这条路,你应该小心。

另一种更安全但更笨拙的方法是实现自定义AccessDeniedHandler并在使用403响应之前自定义错误消息。AccessDecisionManager为您提供可用于检索请求URI的当前AccessDeniedHandler。但是,在这种情况下的坏消息是,您需要URI来进行权限映射才能找到丢失的权限。

答案 1 :(得分:0)

我已经实现了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;
    }
}

该处理程序已在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();
    }
}

请注意,如果还有带有@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;
    }
}