前言
本篇文章的代码较多,但是核心的点是: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