如何在Spring Boot REST API中捕获AccessDeniedException

7

我有一个简单的Spring Boot REST API,其中有两个端点,一个受保护,一个不受保护。对于那个受保护的端点,我想捕获AccessDeniedException并发送401而不是默认的500错误。这是我的安全配置:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{

@Override
public void configure(WebSecurity webSecurity) {
    webSecurity.ignoring().antMatchers("/");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .exceptionHandling()
            .accessDeniedHandler(new AccessDeniedHandler() {
                @Override
                public void handle(HttpServletRequest request, HttpServletResponse response, org.springframework.security.access.AccessDeniedException accessDeniedException) throws IOException, ServletException {
                    System.out.println("I am here now!!!");
                }
            });

    http
            .addFilterAfter(getSecurityFilter(), FilterSecurityInterceptor.class);
    http
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    http
            .csrf()
            .disable();
    http
            .authorizeRequests()
            .antMatchers("/protected").anonymous();
}

public Filter getSecurityFilter() {
    return new Filter() {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            //do nothing here
        }

        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            String appKeyHeaderValue = ((HttpServletRequest)request).getHeader("X-AppKey");
            if(appKeyHeaderValue!=null && appKeyHeaderValue.equals("MY_KEY")) {
                chain.doFilter(request,response);
            } else {
                throw new AccessDeniedException("Access denied man");
            }
        }

        @Override
        public void destroy() {

        }
    };
}

我从来没有看到我的I am here now!!!打印语句,而是看到了默认页面:

Whitelabel Error Page This application has no explicit mapping for /error, so you are seeing this as a fallback. Tue Jul 25 23:21:15 CDT 2017 There was an unexpected error (type=Internal Server Error, status=500). Access denied man

注意当异常被抛出时,我的Access denied man确实被打印出来。

当我运行项目时,控制台还会显示以下内容:

2017-07-25 23:21:14.818 INFO 3872 --- [ restartedMain] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest) 2017-07-25 23:21:14.818 INFO 3872 --- [ restartedMain] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)

这是我的项目结构:

enter image description here


在 dofilter(.....) 方法中,你能否设置状态码 401 而不是 'throw new AccessDeniedException("Access denied man")'? - jishnu radhakrishnan
我可以尝试这样做,但最终目标是拥有一个处理所有异常(包括“AccessDeniedException”)的类。 - El Moreno
所以你需要在一个类中处理所有的异常,对吧?写一个配置类,继承WebMvcConfigurerAdapter,然后添加这个重写方法:@Override public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {exceptionResolvers.add(new CustomHandlerExceptionResolver());} - jishnu radhakrishnan
2个回答

9

我使用了一个扩展了ResponseEntityExceptionHandler的自定义类,并添加了一些注解。

你只需要创建这样一个类:

@ControllerAdvice
public class CustomResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(AccessDeniedException.class)
    public final ResponseEntity<ErrorMessage> handleAccessDeniedException(AccessDeniedException ex, WebRequest request) {
        ErrorMessage errorDetails = new ErrorMessage(new Date(), ex.getMessage(), request.getDescription(false));
        return new ResponseEntity<>(errorDetails, HttpStatus.FORBIDDEN);
    }

}

在这种情况下,答案可能会是以下内容:
{
    "timestamp": "2018-12-28T14:25:23.213+0000",
    "message": "Access is denied",
    "details": "uri=/user/list"
}

1
你为什么在这里也使用了 @RestController? - FishingIsLife
7
这个答案是错误的。访问被拒绝的异常是在安全过滤器链中抛出的,在控制器方法被调用之前就已经抛出了。它们不会被您的异常处理程序捕获,因为它只捕获由控制器方法抛出的异常。 - Rens Verhage
1
只有在使用方法安全性时才有效,即在控制器类或控制器方法级别上使用@PreAuthorize注释,而不是在安全配置中使用antMatcher。 - Matthias Wiedemann
过滤器抛出的AccessDeniedExceptions异常在Spring中会自动转换为403错误。但是,如果使用像RestAssured这样的框架进行测试,则不会发生自动转换。此时,它会进入异常处理程序。 - dstibbe

3

正如@Afridi所建议的那样,异常在到达控制器之前就发生了,因此必须在过滤器链中处理。我建议按照以下步骤进行:

public class AccessDeniedExceptionFilter extends OncePerRequestFilter {

    @Override
    public void doFilterInternal(HttpServletRequest req, HttpServletResponse res, 
                                FilterChain fc) throws ServletException, IOException {
        try {
            fc.doFilter(request, response);
        } catch (AccessDeniedException e) {
         // log error if needed here then redirect
     RequestDispatcher requestDispatcher = 
             getServletContext().getRequestDispatcher(redirecturl);
     requestDispatcher.forward(request, response);

    }
}

在过滤器链中添加此过滤器。
protected void configure(HttpSecurity http) throws Exception {
http
....
.addFilterAfter(httpClientFilter(), AccessDeniedExceptionFilter.class)

修改为 @ControllerAdvice(annotations=RestController.class),并尝试运行。 - fg78nc
同样的结果,我尝试添加@ControllerAdvice(annotations=RestController.class)并删除implements AccessDeniedHandler行,还删除了.exceptionHandling().accessDeniedHandler(new MyExceptionHandler());,但它没有起作用。我还尝试将implements AccessDeniedHandler添加到我的MyExceptionHandler类中,并重新添加.exceptionHandling().accessDeniedHandler(new MyExceptionHandler());,但仍然没有成功。 - El Moreno
抱歉回复晚了。嵌套异常基本上与我上面发布的描述相同。我会接受你的答案,但我已经放弃了这种方法。 - El Moreno
2
httpClientFilter() 是从哪个包中来的?谢谢。 - André Gasser
2
httpClientFilter()是什么? - Bel
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接