Spring MVC(或 Spring Boot)。针对安全相关异常(例如 401 Unauthorized 或 403 Forbidden)的自定义 JSON 响应)

Posted

技术标签:

【中文标题】Spring MVC(或 Spring Boot)。针对安全相关异常(例如 401 Unauthorized 或 403 Forbidden)的自定义 JSON 响应)【英文标题】:Spring MVC (or Spring Boot). Custom JSON response for security related exceptions like 401 Unauthorized or 403 Forbidden) 【发布时间】:2018-05-20 15:58:03 【问题描述】:

我正在开发 REST 服务。它使用 JSON 并且在出现问题时必须返回一些预定义的 JSON 对象。默认的 Spring 响应如下所示:


  "timestamp": 1512578593776,
  "status": 403,
  "error": "Forbidden",
  "message": "Access Denied",
  "path": "/swagger-ui.html"

我想用自己的 JSON 替换这个默认 JSON(带有堆栈跟踪和额外的异常相关信息)。

Spring 提供了一种方便的方法来覆盖默认行为。应该使用自定义异常处理程序定义 @RestControllerAdvice bean。像这样

@RestControllerAdvice
public class GlobalExceptionHandler 
  @ExceptionHandler(value = Exception.class)
  public ResponseEntity<ExceptionResponse> unknownException(Exception ex) 
    ExceptionResponse resp = new ExceptionResponse(ex, level); // my custom response object
    return new ResponseEntity<ExceptionResponse>(resp, resp.getStatus());
  
  @ExceptionHandler(value = AuthenticationException.class)
  public ResponseEntity<ExceptionResponse> authenticationException(AuthenticationExceptionex) 
      // WON'T WORK
  

然后,Spring 使用特殊的消息转换器将自定义 ExceptionResponse 对象转换为 JSON。

问题是,像InsufficientAuthenticationException 这样的安全异常不能被注释为@ExceptionHandler 的方法拦截。此类异常发生在进入 Spring MVC 调度程序 servlet 并初始化所有 MVC 处理程序之前。

可以使用自定义过滤器拦截此异常并从头开始构建自己的 JSON 序列化。在这种情况下,我们会得到一个完全独立于 Spring MVC 基础设施其余部分的代码。这不好。

我找到的解决方案似乎可行,但看起来很疯狂。

@Configuration
public class CustomSecurityConfiguration extends 
WebSecurityConfigurerAdapter 

@Autowired
protected RequestMappingHandlerAdapter requestMappingHandlerAdapter;

@Autowired
protected GlobalExceptionHandler exceptionHandler;

@Override
protected void configure(HttpSecurity http) throws Exception 

    http.authorizeRequests()
        .anyRequest()
        .fullyAuthenticated();

    http.exceptionHandling()
        .authenticationEntryPoint(authenticationEntryPoint());


public AuthenticationEntryPoint authenticationEntryPoint() 
    return new AuthenticationEntryPoint() 
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response,
                AuthenticationException authException) throws IOException, ServletException 

            try 
                ResponseEntity<ExceptionResponse> objResponse = exceptionHandler.authenticationException(authException);

                Method unknownException = exceptionHandler.getClass().getMethod("authenticationException", AuthenticationException.class);

                HandlerMethod handlerMethod = new HandlerMethod(exceptionHandler, unknownException);

                MethodParameter returnType = handlerMethod.getReturnValueType(objResponse);

                ModelAndViewContainer mvc = new ModelAndViewContainer(); // not really used here.

                List<HttpMessageConverter<?>> mconverters = requestMappingHandlerAdapter.getMessageConverters();

                DispatcherServletWebRequest webRequest = new DispatcherServletWebRequest(request, response);

                HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(mconverters);

                processor.handleReturnValue(objResponse, returnType, mvc, webRequest);
             catch (IOException e) 
                throw e;
             catch (RuntimeException e) 
                throw e;                    
             catch (Exception e) 
                throw new ServletException(e);
            
        
    ;

有没有办法使用看起来比这更好的 Spring 序列化管道(使用 Spring 内置消息转换器、MIME 格式协商等)?

【问题讨论】:

这里有一个更优雅的解决方案,非常像你的,但将原始异常包装成一个允许被@ExceptionHandler“捕获”的异常:***.com/questions/43632565/… baeldung.com/spring-security-custom-access-denied-page 【参考方案1】:

Spring 通过AccessDeniedHandler 为异常处理提供开箱即用的支持,可以按照以下步骤使用它来在AccessDeniedExceptionHTTP 403 的情况下实现自定义JSON 响应

实现如下所示的自定义处理程序

public class CustomAccessDeniedHandler implements AccessDeniedHandler 

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exc)
        throws IOException, ServletException 
        System.out.println("Access denied .. ");
        // do something
        response.sendRedirect("/deny");
    

接下来在配置中创建此处理程序的 bean 并将其提供给 spring 安全异常处理程序(重要说明 - 确保从身份验证中排除 /deny 否则请求将无限继续解决错误)

@Bean
public AccessDeniedHandler accessDeniedHandler()
    return new CustomAccessDeniedHandler();


@Override
protected void configure(HttpSecurity http) throws Exception 
    http.cors().and()
            // some other configuration
            .antMatchers("/deny").permitAll()
            .and()
            .exceptionHandling().accessDeniedHandler(accessDeniedHandler());

接下来在控制器类中编写一个对应于/deny 的处理程序,并简单地抛出一个SomeException 的新实例(或任何其他适合的Exception),它将在@987654331 中被拦截@各自的处理程序。

@GetMapping("/deny")
public void accessDenied()
    throw new SomeException("User is not authorized");

如果需要更多信息,请在 cmets 中告知。

【讨论】:

这个方案的问题是重定向。您失去了错误上下文。需要将所有相关信息放入“拒绝”页面的 URL 参数中或使用 HTTP 会话:-(【参考方案2】:

我认为下一个配置应该可以了,请尝试一下。

@Autowired
private HandlerExceptionResolver handlerExceptionResolver;

public AuthenticationEntryPoint authenticationEntryPoint() 
    return new AuthenticationEntryPoint() 
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response,
                             AuthenticationException authException) throws IOException, ServletException 

            try 
                handlerExceptionResolver.resolveException(request, response, null, authException);
             catch (RuntimeException e) 
                throw e;
             catch (Exception e) 
                throw new ServletException(e);
            
        
    ;

【讨论】:

效果很好。您只需在 @ControllerAdvice 注释类中处理 @ExceptionHandler 中的 AuthenticationException 即可与其他错误一起管理此错误。【参考方案3】:

创建 Bean 类

@Component public class AuthenticationExceptionHandler implements AuthenticationEntryPoint, Serializable

并覆盖commence() method 并使用对象映射器创建一个json响应,如下例所示

 ObjectMapper mapper = new ObjectMapper();
 String responseMsg = mapper.writeValueAsString(responseObject);
 response.getWriter().write(responseMsg);

@Autowire AuthenticationExceptionHandlerSecurityConfiguration 类和configure(HttpSecurity http) method 中添加以下行

        http.exceptionHandling()
            .authenticationEntryPoint(authenticationExceptionHandler) 

这样,您应该能够发送自定义 json 响应 401 /403。同上,你可以使用AccessDeniedHandler。让我们知道这是否有助于您解决问题。

【讨论】:

以上是关于Spring MVC(或 Spring Boot)。针对安全相关异常(例如 401 Unauthorized 或 403 Forbidden)的自定义 JSON 响应)的主要内容,如果未能解决你的问题,请参考以下文章

spring boot和mvc区别

Spring Cloud和Spring Boot的区别

我们可以在一个项目中同时使用 Java Spring mvc 和 Spring Boot 吗?

无法使用 Spring Boot 和 MVC 找到/渲染 JSP 文件

Spring MVC 以及 Spring Boot

在 spring-boot 项目中使用 spring mvc xml 项目