spring-security 带有@预授权和@@ControllerAdvice的自定义错误消息

ufj5ltwl  于 2022-11-11  发布在  Spring
关注(0)|答案(4)|浏览(283)

我们正在使用spring和spring-security-3.2。最近我们正在向RestAPI添加注解@PreAuthorize(以前是基于URL的)。

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

我们已经有了全局异常处理程序,它用- @ControllerAdvice和自定义PermissionEvaluator进行了注解,除了错误消息外,一切都正常。
假设某个用户在没有"ViewSalesOrder“权限的情况下访问API,那么默认情况下,Spring会抛出异常”Access is denied“,但没有说明缺少哪个权限(我们需要说明缺少哪个权限)。
是否可能引发一个异常,其中还包括权限名称,因此最终错误消息应该看起来像“访问被拒绝,您需要ViewSalesOrder权限”(此处权限名称应该来自@PreAuthorize注解)?
请注意,我们有100个这样的restAPI,所以通用的解决方案将高度赞赏。

66bbxpm5

66bbxpm51#

由于PermissionEvaluator接口不允许将缺少的权限与求值结果沿着传递,因此没有一种很好的方法可以达到您的期望。
此外,AccessDecisionManager决定关于AccessDecisionVoter示例的投票的最终授权,其中一个示例是PreInvocationAuthorizationAdviceVoter,其关于@PreAuthorize值的评估进行投票。
长话短说,当您的自定义PermissionEvaluator返回falsehasPermission调用时,PreInvocationAuthorizationAdviceVoter投票反对请求(给请求-1分)。
另一方面,您可以尝试一些变通方法来实现您想要的结果。
一种方法是在权限检查失败时在自定义的PermissionEvaluator中抛出一个异常。您可以使用此异常将缺少的权限传播到全局异常处理程序中。您可以将缺少的权限作为参数传递给消息描述符。请注意,这将暂停AccessDecisionManager的执行进程,这意味着将不会执行后续的投票器(默认值为RoleVoterAuthenticatedVoter)。如果选择此路径,则应小心。
另一种更安全但更笨拙的方法是实现一个自定义的AccessDeniedHandler,并在响应403之前自定义错误消息。AccessDeniedHandler提供了当前的HttpServletRequest,可用于检索请求URI。然而,在这种情况下,坏消息是,您需要一个URI到权限的Map,以便找到丢失的权限。

kyvafyod

kyvafyod2#

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

uurv41yg3#

在EL-Expression内部调用的方法可能会抛出 org.springframework.security.access.AccessDeniedException

@PreAuthorize("@myBean.myMethod(#myRequestParameter)")
9bfwbjaz

9bfwbjaz4#

理想情况下,@PreAuthorize注解除了支持SpEl值之外,还应该支持String message();。但是,不管出于什么原因,它并不支持。这里的大多数建议似乎都不必要地繁琐和复杂。正如@lathspell所建议的,提供您自己的错误消息沿着任何自定义访问验证逻辑的最简单方法是就是添加一个简单的方法来执行检查,并在检查失败时抛出AccessDeniedException,然后在SpEl表达式中引用该方法。

@RestController
@RequiredArgsConstructor // if you use lombok
public class OrderController {  

    private final OrderService orderService;  
    ...

    @GetMapping(value = "/salesorders", produces = MediaType.APPLICATION_JSON_VALUE)
    @PreAuthorize("@orderController.hasPermissionToSeeOrders(#someArgOfThisMethod)")
    public Page<OrderDto> getSalesOrders(
                          // someArgOfThisMethod here, perhaps HttpRequest, @PathVariable, @RequestParam, etc. 
                          int pageIndex, int pageSize, String sortBy, String sortOrder) {
        Pageable pageRequest = PageRequest.of(pageIndex, pageSize, Sort.Direction.fromString(sortOrder), sortBy);
        return ordersService.retrieveSalesOrders(..., pageRequest);
    }

    public static Boolean hasPermissionToSeeOrders(SomeArgOfTheTargetMethod argToEvaluate) {
        //check eligibility to perform the operation based on some data from the incoming objects (argToEvaluate)
        if (condition fails) {
            throw new AccessDeniedException("Your message");
        }
        return true;
    }

相关问题