spring cloud oauth2 自定义异常返回格式之资源服务器异常

823 阅读3分钟

本文主要记录如何自定义spring cloud oauth2资源服务器异常

想必用过spring cloud oauth2的都知道它默认的返回格式,在实际开发中不是很友好,通常情况下我们都是需要统一的数据返回格式,这样才可以不被前端大哥们吐槽

废话不过说直接开始正文了

源码解析

  • 资源服务异常主要是由ExceptionTranslationFilter过滤器拦截处理
  • ExceptionTranslationFilter捕获到异常信息后,尝试获取异常栈中的异常信息, 根据异常信息做出不同的处理方式,如果是ServletException则直接抛出去,认证异常则交予authenticationEntryPoint处理,资源权限异常则交予accessDeniedHandler处理
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
   HttpServletRequest request = (HttpServletRequest) req;
   HttpServletResponse response = (HttpServletResponse) res;

   try {
      chain.doFilter(request, response);

      logger.debug("Chain processed normally");
   }
   catch (IOException ex) {
      throw ex;
   }
   catch (Exception ex) {
      // Try to extract a SpringSecurityException from the stacktrace
      
      //获取异常栈中的异常信息,
      Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
      
      //获取异常信息中的AuthenticationException认证异常信息
      RuntimeException ase = (AuthenticationException) throwableAnalyzer
            .getFirstThrowableOfType(AuthenticationException.class, causeChain);

      //当ase == null则说明没有获取到认证异常,则尝试获取资源权限异常信息AccessDeniedException
      if (ase == null) {
         ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
               AccessDeniedException.class, causeChain);
      }

      //ase != null 则说明异常属于资源或者认证异常中的一种,否则就将异常抛出去
      if (ase != null) {
         if (response.isCommitted()) {
            throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
         }
         handleSpringSecurityException(request, response, chain, ase);
      }
      else {
         // Rethrow ServletExceptions and RuntimeExceptions as-is
         if (ex instanceof ServletException) {
            throw (ServletException) ex;
         }
         else if (ex instanceof RuntimeException) {
            throw (RuntimeException) ex;
         }

         // Wrap other Exceptions. This shouldn't actually happen
         // as we've already covered all the possibilities for doFilter
         throw new RuntimeException(ex);
      }
   }
}
private void handleSpringSecurityException(HttpServletRequest request,
      HttpServletResponse response, FilterChain chain, RuntimeException exception)
      throws IOException, ServletException {
      
      //判断当前异常是否为AuthenticationException异常
   if (exception instanceof AuthenticationException) {
      logger.debug(
            "Authentication exception occurred; redirecting to authentication entry point",
            exception);

      sendStartAuthentication(request, response, chain,
            (AuthenticationException) exception);
   }
   //当前对象为AccessDeniedException 则调用accessDeniedHandler处理异常
   else if (exception instanceof AccessDeniedException) {
      Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
      if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
         logger.debug(
               "Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
               exception);

         sendStartAuthentication(
               request,
               response,
               chain,
               new InsufficientAuthenticationException(
                  messages.getMessage(
                     "ExceptionTranslationFilter.insufficientAuthentication",
                     "Full authentication is required to access this resource")));
      }
      else {
         logger.debug(
               "Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
               exception);

         accessDeniedHandler.handle(request, response,
               (AccessDeniedException) exception);
      }
   }
}
authenticationEntryPoint处理异常
//调用authenticationEntryPoint处理异常
protected void sendStartAuthentication(HttpServletRequest request,
      HttpServletResponse response, FilterChain chain,
      AuthenticationException reason) throws ServletException, IOException {
   // SEC-112: Clear the SecurityContextHolder's Authentication, as the
   // existing Authentication is no longer considered valid
   SecurityContextHolder.getContext().setAuthentication(null);
   requestCache.saveRequest(request, response);
   logger.debug("Calling Authentication entry point.");
   authenticationEntryPoint.commence(request, response, reason);
}

通过源码我们可以得知认证异常和资源权限异常分别交予不同的类去处理,那么我们想要自定义异常返回信息的话,则需要自定义类去实现authenticationEntryPoint和自定义类去实现accessDeniedHandler,然后替换资源服务器默认的异常处理类就可以了

废话不多说,直接上干货

  • 自定义资源权限异常类实现AccessDeniedHandler接口,返回自定义格式
@Slf4j
public class LwAccessDeniedHandler implements AccessDeniedHandler {

    @Resource
    private ObjectMapper objectMapper;


    /**
     * 返回自定义格式格式 R
     *
     * @param request
     * @param response
     * @param accessDeniedException
     */
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        log.error("全局异常处理:{}", accessDeniedException.getMessage());
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(CommonConstants.UTF_8);
        PrintWriter writer = response.getWriter();
        writer.write(objectMapper.writeValueAsString(R.failed(accessDeniedException.getMessage())));
    }
}
  • 自定义认证异常类实现AccessDeniedHandler接口,返回自定义格式
@Slf4j
public class LwAuthenticationEntryPoint extends AbstractOAuth2SecurityExceptionHandler implements AuthenticationEntryPoint {

    @Resource
    private ObjectMapper objectMapper;


    /**
     * 返回自定义格式格式 R
     * @param request
     * @param response
     * @param e
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        log.error("全局异常处理:{}", e.getMessage());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(CommonConstants.UTF_8);
        response.setStatus(HttpStatus.OK.value());
        PrintWriter writer = response.getWriter();
        writer.write(objectMapper.writeValueAsString(R.failed(e.getMessage())));
    }
}

通过资源服务器配置文件注入自定义异常处理类,替换默认异常处理

@Override
public void configure(ResourceServerSecurityConfigurer resource) throws Exception {
    resource.tokenStore(tokenStore)
            .authenticationEntryPoint(new LwAuthenticationEntryPoint())
            .accessDeniedHandler(new LwAccessDeniedHandler())
            .stateless(true);
}

效果

image.png

最后吐槽一下spring cloud oauth 的异常处理,一点都不人性化