(七) OAuth 2.0 自定义异常处理格式

前言

本篇文章的代码较多,但是核心的点是:exceptionTranslator、authenticationEntryPoint

只要百度搜索关键字基本上都会关于这两个的介绍

exceptionTranslator

/**

  • @description: oauth2 认证服务异常处理,重写oauth2的默认实现

  • @Author C_Y_J

  • @create 2022/1/12 9:48
    **/
    public class CustomWebResponseExceptionTranslator implements WebResponseExceptionTranslator {

    private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();

    @Override
    public ResponseEntity translate(Exception e) {

     // Try to extract a SpringSecurityException from the stacktrace
     // 尝试从堆栈跟踪中提取 SpringSecurityException
     Throwable[] causeChain = throwableAnalyzer.determineCauseChain(e);
    
     // 未经授权的异常
     Exception ase = (AuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
     if (ase != null) {
         return handleOAuth2Exception(new UnauthorizedException(ase.getMessage(), ase));
     }
    
     // 访问被拒绝异常
     ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
     if (ase != null) {
         return handleOAuth2Exception(new UnauthorizedException.ForbiddenException(ase.getMessage(), ase));
     }
    
     // InvalidGrantException  grant_type不支持
     ase = (InvalidGrantException) throwableAnalyzer.getFirstThrowableOfType(InvalidGrantException.class, causeChain);
     if (ase != null) {
         String msg = SecurityMessageSourceUtil.getAccessor().getMessage(
                 "AbstractUserDetailsAuthenticationProvider.badCredentials", ase.getMessage(), Locale.CHINA);
    
         return handleOAuth2Exception(new UnauthorizedException.InvalidException(msg, ase));
     }
    
     // HttpRequestMethodNotSupportedException
     ase = (HttpRequestMethodNotSupportedException) throwableAnalyzer.getFirstThrowableOfType(HttpRequestMethodNotSupportedException.class, causeChain);
     if (ase != null) {
         return handleOAuth2Exception(new UnauthorizedException.MethodNotAllowedException(ase.getMessage(), ase));
     }
    
     // 处理不合法的令牌错误 427 返回
     ase = (InvalidTokenException) throwableAnalyzer.getFirstThrowableOfType(InvalidTokenException.class, causeChain);
     if (ase != null) {
         return handleOAuth2Exception(new UnauthorizedException.TokenInvalidException(ase.getMessage(), ase));
     }
    
     ase = (OAuth2Exception) throwableAnalyzer.getFirstThrowableOfType(OAuth2Exception.class, causeChain);
     if (ase != null) {
         return handleOAuth2Exception((OAuth2Exception) ase);
     }
    
     // 这个最后一个异常,默认为服务器挂了
     String reasonPhrase = HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase();
     return handleOAuth2Exception(new UnauthorizedException.ServerErrorException(reasonPhrase, e));
    

    }

    private ResponseEntity handleOAuth2Exception(OAuth2Exception e) {
    int status = e.getHttpErrorCode();
    // 客户端异常直接返回客户端,不然无法解析
    if (e instanceof ClientAuthenticationException) {
    return new ResponseEntity<>(e, HttpStatus.valueOf(status));
    }

     CustomOAuth2Exception exception = new CustomOAuth2Exception(e.getMessage(), e.getOAuth2ErrorCode());
     return new ResponseEntity<>(exception, HttpStatus.valueOf(status));
    

    }

}

/**

  • @description: 序列化实现

  • @Author C_Y_J

  • @create 2022/1/12 10:42
    **/
    public class CustomOAuth2ExceptionSerializer extends StdSerializer {

    public CustomOAuth2ExceptionSerializer() {
    super(CustomOAuth2Exception.class);
    }

    @Override
    @SneakyThrows
    public void serialize(CustomOAuth2Exception value, JsonGenerator gen, SerializerProvider provider) {
    gen.writeStartObject();
    gen.writeObjectField(“code”, 1);
    gen.writeStringField(“msg”, value.getMessage());
    gen.writeStringField(“data”, value.getErrorCode());
    gen.writeEndObject();
    }
    }

/**

  • @description: 添加自定义OAuth2异常类,并指定json序列化方式

  • @Author C_Y_J

  • @create 2022/1/12 10:38
    **/
    @JsonSerialize(using = CustomOAuth2ExceptionSerializer.class)
    public class CustomOAuth2Exception extends OAuth2Exception {

    @Getter
    private String errorCode;

    public CustomOAuth2Exception(String msg) {
    super(msg);
    }

    public CustomOAuth2Exception(String msg, Throwable t) {
    super(msg, t);
    }

    public CustomOAuth2Exception(String msg, String errorCode) {
    super(msg);
    this.errorCode = errorCode;
    }

}

/**

  • @description: 未经授权的异常

  • @Author C_Y_J

  • @create 2022/1/12 9:52
    **/
    public class UnauthorizedException extends CustomOAuth2Exception {

    public UnauthorizedException(String msg, Throwable t) {
    super(msg);
    }

    @Override
    public String getOAuth2ErrorCode() {
    return “unauthorized”;
    }

    @Override
    public int getHttpErrorCode() {
    return HttpStatus.UNAUTHORIZED.value();
    }

    /**

    • @description: 认为服务挂了

    • @Author C_Y_J

    • @create 2022/1/12 9:52
      **/
      public static class ServerErrorException extends CustomOAuth2Exception {

      public ServerErrorException(String msg, Throwable t) {
      super(msg);
      }

      @Override
      public String getOAuth2ErrorCode() {
      return “server_error”;
      }

      @Override
      public int getHttpErrorCode() {
      return HttpStatus.INTERNAL_SERVER_ERROR.value();
      }
      }

    /**

    • @description: 禁止访问

    • @Author C_Y_J

    • @create 2022/1/12 9:52
      **/
      public static class ForbiddenException extends CustomOAuth2Exception {

      public ForbiddenException(String msg, Throwable t) {
      super(msg, t);
      }

      @Override
      public String getOAuth2ErrorCode() {
      return “access_denied”;
      }

      @Override
      public int getHttpErrorCode() {
      return HttpStatus.FORBIDDEN.value();
      }

    }

    /**

    • @description: grant_type不支持

    • @Author C_Y_J

    • @create 2022/1/12 9:52
      **/
      public static class InvalidException extends CustomOAuth2Exception {

      public InvalidException(String msg, Throwable t) {
      super(msg);
      }

      @Override
      public String getOAuth2ErrorCode() {
      return “invalid_exception”;
      }

      @Override
      public int getHttpErrorCode() {
      return 426;
      }
      }

    /**

    • @description:

    • @Author C_Y_J

    • @create 2022/1/12 9:52
      **/
      public static class MethodNotAllowedException extends CustomOAuth2Exception {

      public MethodNotAllowedException(String msg, Throwable t) {
      super(msg);
      }

      @Override
      public String getOAuth2ErrorCode() {
      return “method_not_allowed”;
      }

      @Override
      public int getHttpErrorCode() {
      return HttpStatus.METHOD_NOT_ALLOWED.value();
      }
      }

    /**

    • @description:

    • @Author C_Y_J

    • @create 2022/1/12 9:52
      **/
      public static class TokenInvalidException extends CustomOAuth2Exception {

      public TokenInvalidException(String msg, Throwable t) {
      super(msg);
      }

      @Override
      public String getOAuth2ErrorCode() {
      return “invalid_token”;
      }

      @Override
      public int getHttpErrorCode() {
      return HttpStatus.FAILED_DEPENDENCY.value();
      }
      }

}

授权服务器中的错误处理使用标准的Spring MVC功能,即@ExceptionHandler端点本身中的方法。但是其原生的异常信息可能与我们实际使用的异常处理不一致,需要进行转义。可以自定义WebResponseExceptionTranslator,想授权端点提供异常处理,这是更改响应异常处理的最佳方法。这里重写了 WebResponseExceptionTranslator 的默认实现,可以去对比源码的实现。为了简洁我把很多类写在一起,实际在使用的过程中也可以拆开这些类。

/**

  • @description:

  • @Author C_Y_J

  • @create 2022/1/12 9:40
    **/
    public class SecurityMessageSourceUtil extends ReloadableResourceBundleMessageSource {

    public SecurityMessageSourceUtil() {
    setBasename(“classpath:messages/messages”);
    setDefaultLocale(Locale.CHINA);
    }

    public static MessageSourceAccessor getAccessor() {
    return new MessageSourceAccessor(new SecurityMessageSourceUtil());
    }
    }

AuthorizationServerConfiguration

private final CustomWebResponseExceptionTranslator webResponseExceptionTranslator;

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    // 配置端点
    endpoints.tokenStore(tokenStore)
            .authenticationManager(authenticationManager)
            .userDetailsService(userDetailsService);

    // 授权服务器端点的自定义异常处理
    endpoints.exceptionTranslator(webResponseExceptionTranslator);

}

authenticationEntryPoint

/**

  • @description: 针对资源服务器的异常处理 {@link OAuth2AuthenticationProcessingFilter}不同细化异常处理

  • @Author C_Y_J

  • @create 2022/1/12 9:20
    **/
    @AllArgsConstructor
    public class CustomOAuth2AuthenticationEntryPoint implements AuthenticationEntryPoint {

    private final ObjectMapper objectMapper;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

     response.setCharacterEncoding("UTF-8");
     response.setContentType(MediaType.APPLICATION_JSON_VALUE);
     response.setStatus(HttpStatus.UNAUTHORIZED.value());
    
     // 用自己公司的封装工具类,仅供参看
     R<String> result = new R<>();
     result.setMsg(authException.getMessage());
     result.setData(authException.getMessage());
     result.setCode(1);
    
    
     if (authException instanceof CredentialsExpiredException || authException instanceof InsufficientAuthenticationException) {
         String msg = SecurityMessageSourceUtil.getAccessor().getMessage(
                 "AbstractUserDetailsAuthenticationProvider.credentialsExpired", authException.getMessage());
    
         result.setMsg(msg);
     }
    
     if (authException instanceof UsernameNotFoundException) {
         String msg = SecurityMessageSourceUtil.getAccessor().getMessage(
                 "AbstractUserDetailsAuthenticationProvider.noopBindAccount", authException.getMessage());
    
         result.setMsg(msg);
     }
    
     if (authException instanceof BadCredentialsException) {
         String msg = SecurityMessageSourceUtil.getAccessor().getMessage(
                 "AbstractUserDetailsAuthenticationProvider.badClientCredentials", authException.getMessage());
    
         result.setMsg(msg);
     }
    
     if (authException instanceof InsufficientAuthenticationException) {
         String msg = SecurityMessageSourceUtil.getAccessor()
                 .getMessage("AbstractAccessDecisionManager.expireToken", authException.getMessage());
         // 默认是401,因为前后端约定令牌过期的状态码为424
         response.setStatus(HttpStatus.FAILED_DEPENDENCY.value());
         result.setMsg(msg);
     }
    
     PrintWriter printWriter = response.getWriter();
     printWriter.append(objectMapper.writeValueAsString(result));
    

    }
    }

@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class R implements Serializable {

private static final long serialVersionUID = 1L;

/**
 * 返回标记:成功标记=0,失败标记=1
 */
@Getter
@Setter
private int code;

/**
 * 返回信息
 */
@Getter
@Setter
private String msg;

/**
 * 数据
 */
@Getter
@Setter
private T data;

public static <T> R<T> ok() {
    return restResult(null, 0, null);
}

public static <T> R<T> ok(T data) {
    return restResult(data, 0, null);
}

public static <T> R<T> ok(T data, String msg) {
    return restResult(data, 0, msg);
}

public static <T> R<T> failed() {
    return restResult(null, 1, null);
}

public static <T> R<T> failed(String msg) {
    return restResult(null, 1, msg);
}

public static <T> R<T> failed(T data) {
    return restResult(data, 1, null);
}

public static <T> R<T> failed(T data, String msg) {
    return restResult(data, 1, msg);
}

private static <T> R<T> restResult(T data, int code, String msg) {
    R<T> apiResult = new R<>();
    apiResult.setCode(code);
    apiResult.setData(data);
    apiResult.setMsg(msg);
    return apiResult;
}

}

ResourceServerConfiguration

private final CustomOAuth2AuthenticationEntryPoint auth2AuthenticationEntryPoint;

@Override
public void configure(ResourceServerSecurityConfigurer resources) {

    // 设置资源服务器的资源列表
    resources.resourceId("resource");

    // 设置资源服务器的token存储
    resources.tokenStore(tokenStore);

    // 针对资源服务器的异常处理
    resources.authenticationEntryPoint(auth2AuthenticationEntryPoint);

}

总结

● 当访问未纳入Oauth2保护资源或者访问授权端点时客户端验证失败,抛出异常,AuthenticationEntryPoint. Commence(…)就会被调用。这个对应的代码在ExceptionTranslationFilter中,当ExceptionTranslationFilter catch到异常后,就会间接调用AuthenticationEntryPoint。默认使用LoginUrlAuthenticationEntryPoint处理异常,当抛出依次LoginUrlAuthenticationEntryPoint会将异常呈现给授权服务器默认的Login视图。
● 当访问未纳入Oauth2资源管理的接口时,因为应用接入安全框架,因此依旧会进行权限验证,当用户无权访问时会有ExceptionTranslationFilter 拦截异常并将异常呈现到默认的登录视图提示用户登录:
● 当调用授权端点(/oauth/token)时,根据前面的源码我们知道在授权认证前,会先通过客户端验证Filter进行客户端验证,当客户端验证失败会抛出异常并由ExceptionTranslationFilter 拦截,将异常呈现给默认的登录视图:spring:
redis:
host: 127.0.0.1
port: 6379