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

时间:2017-12-06 17:12:55

标签: json spring spring-mvc spring-boot spring-security

我正在开发一个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格式协商等)看起来比这更好?

3 个答案:

答案 0 :(得分:2)

创建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 AuthenticationExcetionHandler类及SecurityConfiguration中的configure(HttpSecurity http) method添加以下行

        http.exceptionHandling()
            .authenticationEntryPoint(authenticationExceptionHandler) 

这样你应该能够发送客户json响应401/403。与上述相同,您可以使用AccessDeniedHandler。如果这有助于解决问题,请告诉我们。

答案 1 :(得分:1)

我认为下一个配置应该有效,请尝试一下。

@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);
            }
        }
    };
}

答案 2 :(得分:0)

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");
    }
}

接下来在config中创建此处理程序的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适合)的新实例,其中将在@RestControllerAdvice相应的处理程序中截获。

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

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