SpringSecurityOauth2系列学习(五):授权服务自定义异常处理
系列导航
SpringSecurity系列
- SpringSecurity系列学习(一):初识SpringSecurity
- SpringSecurity系列学习(二):密码验证
- SpringSecurity系列学习(三):认证流程和源码解析
- SpringSecurity系列学习(四):基于JWT的认证
- SpringSecurity系列学习(四-番外):多因子验证和TOTP
- SpringSecurity系列学习(五):授权流程和源码分析
- SpringSecurity系列学习(六):基于RBAC的授权
SpringSecurityOauth2系列
- SpringSecurityOauth2系列学习(一):初认Oauth2
- SpringSecurityOauth2系列学习(二):授权服务
- SpringSecurityOauth2系列学习(三):资源服务
- SpringSecurityOauth2系列学习(四):自定义登陆登出接口
- SpringSecurityOauth2系列学习(五):授权服务自定义异常处理
授权服务异常分析
在 Spring Security Oauth2中,异常是框架自行捕获处理了,使用@RestControllerAdvice
是不能统一处理的,因为这个注解是对controller层进行拦截。
我们先来看看Spring Security Oauth2是怎么处理异常的
OAuth2Exception
OAuth2Exception
类就是Oauth2的异常类,继承自RuntimeException
。
其定义了很多常量表示错误信息,基本上对应每个OAuth2Exception
的子类。
// 错误
public static final String ERROR = "error";
// 错误描述
public static final String DESCRIPTION = "error_description";
// 错误的URI
public static final String URI = "error_uri";
// 无效的请求 InvalidRequestException
public static final String INVALID_REQUEST = "invalid_request";
// 无效客户端
public static final String INVALID_CLIENT = "invalid_client";
// 无效授权 InvalidGrantException
public static final String INVALID_GRANT = "invalid_grant";
// 未经授权的客户端
public static final String UNAUTHORIZED_CLIENT = "unauthorized_client";
// 不受支持的授权类型 UnsupportedGrantTypeException
public static final String UNSUPPORTED_GRANT_TYPE = "unsupported_grant_type";
// 无效授权范围 InvalidScopeException
public static final String INVALID_SCOPE = "invalid_scope";
// 授权范围不足
public static final String INSUFFICIENT_SCOPE = "insufficient_scope";
// 令牌无效 InvalidTokenException
public static final String INVALID_TOKEN = "invalid_token";
// 重定向uri不匹配 RedirectMismatchException
public static final String REDIRECT_URI_MISMATCH ="redirect_uri_mismatch";
// 不支持的响应类型 UnsupportedResponseTypeException
public static final String UNSUPPORTED_RESPONSE_TYPE ="unsupported_response_type";
// 拒绝访问 UserDeniedAuthorizationException
public static final String ACCESS_DENIED = "access_denied";
OAuth2Exception
也定义了很多方法:
// 添加额外的异常信息
private Map<String, String> additionalInformation = null;
// OAuth2 错误代码
public String getOAuth2ErrorCode() {
return "invalid_request";
}
// 与此错误关联的 HTTP 错误代码
public int getHttpErrorCode() {
return 400;
}
// 根据定义好的错误代码(常量),创建对应的OAuth2Exception子类
public static OAuth2Exception create(String errorCode, String errorMessage) {
if (errorMessage == null) {
errorMessage = errorCode == null ? "OAuth Error" : errorCode;
}
if (INVALID_CLIENT.equals(errorCode)) {
return new InvalidClientException(errorMessage);
}
// 省略.......
}
// 从 Map<String,String> 创建一个 {@link OAuth2Exception}。
public static OAuth2Exception valueOf(Map<String, String> errorParams) {
// 省略.......
return ex;
}
/**
* @return 以逗号分隔的详细信息列表(键值对)
*/
public String getSummary() {
// 省略.......
return builder.toString();
}
异常处理源码分析
我们以密码模式,不传入授权类型为例。
1. 端点校验GrantType抛出异常
密码模式访问/oauth/token
端点,在下面代码中,不传入GrantType
,会抛出InvalidRequestException
异常,这个异常的msg为Missing grant type
。
创建的异常,包含了下面这些信息。
2. 端点中的@ExceptionHandler统一处理异常
在端点类TokenEndpoint
中,定义了多个@ExceptionHandler
,所以只要是在这个端点中的异常,都会被捕获处理。
@ExceptionHandler({HttpRequestMethodNotSupportedException.class})
public ResponseEntity<OAuth2Exception> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) throws Exception {
if (this.logger.isInfoEnabled()) {
this.logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
}
return this.getExceptionTranslator().translate(e);
}
@ExceptionHandler({Exception.class})
public ResponseEntity<OAuth2Exception> handleException(Exception e) throws Exception {
if (this.logger.isErrorEnabled()) {
this.logger.error("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage(), e);
}
return this.getExceptionTranslator().translate(e);
}
@ExceptionHandler({ClientRegistrationException.class})
public ResponseEntity<OAuth2Exception> handleClientRegistrationException(Exception e) throws Exception {
if (this.logger.isWarnEnabled()) {
this.logger.warn("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
}
return this.getExceptionTranslator().translate(new BadClientCredentialsException());
}
@ExceptionHandler({OAuth2Exception.class})
public ResponseEntity<OAuth2Exception> handleException(OAuth2Exception e) throws Exception {
if (this.logger.isWarnEnabled()) {
this.logger.warn("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
}
return this.getExceptionTranslator().translate(e);
}
1中抛出的InvalidRequestException
是OAuth2Exception
的子类,所以最终由下面这个ExceptionHandler
处理。
@ExceptionHandler(OAuth2Exception.class)
public ResponseEntity<OAuth2Exception> handleException(OAuth2Exception e) throws Exception {
// 打印WARN日志
if (logger.isWarnEnabled()) {
logger.warn("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
}
// 调用异常翻译器
return getExceptionTranslator().translate(e);
}
3.异常翻译处理器
最终调用WebResponseExceptionTranslator
的实现类,对异常进行翻译封装处理,最后由Spring MVC 返回ResponseEntity< OAuth2Exception>
对象。ResponseEntity
实际是一个HttpEntity
,是Spring WEB提供了一个封装信息响应给请求的对象。
异常翻译默认使用的是DefaultWebResponseExceptionTranslator
类,最终进入其translate
方法。
@Override
public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {
// 1. 尝试从堆栈跟踪中提取 SpringSecurityException
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(e);
Exception ase = (OAuth2Exception) throwableAnalyzer.getFirstThrowableOfType(OAuth2Exception.class, causeChain);
// 2. 获取OAuth2Exception
if (ase != null) {
// 3. 获取到了OAuth2Exception,直接处理
return handleOAuth2Exception((OAuth2Exception) ase);
}
ase = (AuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class,
causeChain);
if (ase != null) {
return handleOAuth2Exception(new UnauthorizedException(e.getMessage(), e));
}
ase = (AccessDeniedException) throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
if (ase instanceof AccessDeniedException) {
return handleOAuth2Exception(new ForbiddenException(ase.getMessage(), ase));
}
ase = (HttpRequestMethodNotSupportedException) throwableAnalyzer.getFirstThrowableOfType(
HttpRequestMethodNotSupportedException.class, causeChain);
if (ase instanceof HttpRequestMethodNotSupportedException) {
return handleOAuth2Exception(new MethodNotAllowed(ase.getMessage(), ase));
}
return handleOAuth2Exception(new ServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), e));
}
真正创建ResponseEntity
的是handleOAuth2Exception
方法。
private ResponseEntity<OAuth2Exception> handleOAuth2Exception(OAuth2Exception e) throws IOException {
// 获取错误码 eg:400
int status = e.getHttpErrorCode();
// 设置响应消息头,禁用缓存
HttpHeaders headers = new HttpHeaders();
headers.set("Cache-Control", "no-store");
headers.set("Pragma", "no-cache");
// 如果是401,或者是范围不足异常,设置WWW-Authenticate 消息头
if (status == HttpStatus.UNAUTHORIZED.value() || (e instanceof InsufficientScopeException)) {
headers.set("WWW-Authenticate", String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, e.getSummary()));
}
// 将异常信息,塞到ResponseEntity的Body中
ResponseEntity<OAuth2Exception> response = new ResponseEntity<OAuth2Exception>(e, headers,
HttpStatus.valueOf(status));
return response;
}
4.序列化
最终ResponseEntity
进行序列化,变成json字符串的时候,OAuth2Exception
通过其定义的序列化器,进行json字符串的转换
OAuth2Exception
上标注了JsonSerialize 、JsonDeserialize
注解,所以会进行序列化操作。主要是将OAuth2Exception
中的异常进行序列化处理。
@Override
public void serialize(OAuth2Exception value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
JsonProcessingException {
jgen.writeStartObject();
// 序列化error
jgen.writeStringField("error", value.getOAuth2ErrorCode());
String errorMessage = value.getMessage();
if (errorMessage != null) {
errorMessage = HtmlUtils.htmlEscape(errorMessage);
}
// 序列化error_description
jgen.writeStringField("error_description", errorMessage);
// 序列化额外的附加信息AdditionalInformation
if (value.getAdditionalInformation()!=null) {
for (Entry<String, String> entry :
value.getAdditionalInformation().entrySet()) {
String key = entry.getKey();
String add = entry.getValue();
jgen.writeStringField(key, add);
}
}
jgen.writeEndObject();
}
5. 前端获取错误信息
最终,OAuth2Exception
经过抛出,ExceptionHandler
捕获,翻译,封装返回ResponseEntity
,序列化处理,就展示给前端了。
自定义授权服务器异常信息
如果只需要改变有异常时,返回的json响应体,那么只需要自定义翻译器即可,不需要自定义异常并添加序列化和反序列化。
但是在实际开放中,一般异常都是有固定格式的,OAuth2Exception
直接返回,不是我们想要的,那么我们可以进行改造。
1.自定义异常
自定义一个异常,继承OAuth2Exception
,并添加序列化
/**
* @author 硝酸铜
* @date 2021/9/23
*/
@JsonSerialize(using = MyOauthExceptionJackson2Serializer.class)
@JsonDeserialize(using = MyOAuth2ExceptionJackson2Deserializer.class)
public class MyOAuth2Exception extends OAuth2Exception {
public MyOAuth2Exception(String msg, Throwable t) {
super(msg, t);
}
public MyOAuth2Exception(String msg) {
super(msg);
}
}
2.编写序列化
参考OAuth2Exception
的序列化,编写我们自己的异常的序列化与反序列化类。
package com.cupricnitrate.authority.exception.serializer;
import com.cupricnitrate.authority.exception.MyOAuth2Exception;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义Oauth2异常类序列化类
* @author 硝酸铜
* @date 2021/9/23
*/
public class MyOauthExceptionJackson2Serializer extends StdSerializer<MyOAuth2Exception> {
public MyOauthExceptionJackson2Serializer() {
super(MyOAuth2Exception.class);
}
@Override
public void serialize(MyOAuth2Exception value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
JsonProcessingException {
jgen.writeStartObject();
Map<String ,String > content = new HashMap<>();
//序列化error
content.put(OAuth2Exception.ERROR,value.getOAuth2ErrorCode());
//jgen.writeStringField(OAuth2Exception.ERROR,value.getOAuth2ErrorCode());
//序列化error_description
content.put(OAuth2Exception.DESCRIPTION,value.getMessage());
//jgen.writeStringField(OAuth2Exception.DESCRIPTION,value.getMessage());
//序列化额外的附加信息AdditionalInformation
if (value.getAdditionalInformation()!=null) {
for (Map.Entry<String, String> entry : value.getAdditionalInformation().entrySet()) {
String key = entry.getKey();
String add = entry.getValue();
content.put(key,add);
//jgen.writeStringField(key, add);
}
}
jgen.writeFieldName("result");
jgen.writeObject(content);
jgen.writeFieldName("code");
jgen.writeNumber(500);
jgen.writeEndObject();
}
}
package com.cupricnitrate.authority.exception.serializer;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.springframework.security.oauth2.common.exceptions.*;
import org.springframework.security.oauth2.common.util.OAuth2Utils;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 自定义Oauth2异常类反序列化类
* @author 硝酸铜
* @date 2021/9/23
*/
public class MyOAuth2ExceptionJackson2Deserializer extends StdDeserializer<OAuth2Exception> {
public MyOAuth2ExceptionJackson2Deserializer() {
super(OAuth2Exception.class);
}
@Override
public OAuth2Exception deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException,
JsonProcessingException {
JsonToken t = jp.getCurrentToken();
if (t == JsonToken.START_OBJECT) {
t = jp.nextToken();
}
Map<String, Object> errorParams = new HashMap<String, Object>();
for (; t == JsonToken.FIELD_NAME; t = jp.nextToken()) {
// Must point to field name
String fieldName = jp.getCurrentName();
// And then the value...
t = jp.nextToken();
// Note: must handle null explicitly here; value deserializers won't
Object value;
if (t == JsonToken.VALUE_NULL) {
value = null;
}
// 复杂结构
else if (t == JsonToken.START_ARRAY) {
value = jp.readValueAs(List.class);
} else if (t == JsonToken.START_OBJECT) {
value = jp.readValueAs(Map.class);
} else {
value = jp.getText();
}
errorParams.put(fieldName, value);
}
//读取error与error_description字段
Object errorCode = errorParams.get(OAuth2Exception.ERROR);
String errorMessage = errorParams.get(OAuth2Exception.DESCRIPTION) != null ? errorParams.get(OAuth2Exception.DESCRIPTION).toString() : null;
if (errorMessage == null) {
errorMessage = errorCode == null ? "OAuth Error" : errorCode.toString();
}
//将读取到的error与error_description字段,生成具体的OAuth2Exception实现类
OAuth2Exception ex;
if (OAuth2Exception.INVALID_CLIENT.equals(errorCode)) {
ex = new InvalidClientException(errorMessage);
} else if (OAuth2Exception.UNAUTHORIZED_CLIENT.equals(errorCode)) {
ex = new UnauthorizedClientException(errorMessage);
} else if (OAuth2Exception.INVALID_GRANT.equals(errorCode)) {
if (errorMessage.toLowerCase().contains("redirect") && errorMessage.toLowerCase().contains("match")) {
ex = new RedirectMismatchException(errorMessage);
} else {
ex = new InvalidGrantException(errorMessage);
}
} else if (OAuth2Exception.INVALID_SCOPE.equals(errorCode)) {
ex = new InvalidScopeException(errorMessage);
} else if (OAuth2Exception.INVALID_TOKEN.equals(errorCode)) {
ex = new InvalidTokenException(errorMessage);
} else if (OAuth2Exception.INVALID_REQUEST.equals(errorCode)) {
ex = new InvalidRequestException(errorMessage);
} else if (OAuth2Exception.REDIRECT_URI_MISMATCH.equals(errorCode)) {
ex = new RedirectMismatchException(errorMessage);
} else if (OAuth2Exception.UNSUPPORTED_GRANT_TYPE.equals(errorCode)) {
ex = new UnsupportedGrantTypeException(errorMessage);
} else if (OAuth2Exception.UNSUPPORTED_RESPONSE_TYPE.equals(errorCode)) {
ex = new UnsupportedResponseTypeException(errorMessage);
} else if (OAuth2Exception.INSUFFICIENT_SCOPE.equals(errorCode)) {
ex = new InsufficientScopeException(errorMessage, OAuth2Utils.parseParameterList((String) errorParams
.get("scope")));
} else if (OAuth2Exception.ACCESS_DENIED.equals(errorCode)) {
ex = new UserDeniedAuthorizationException(errorMessage);
} else {
ex = new OAuth2Exception(errorMessage);
}
//将json中的其他字段添加到OAuth2Exception的附加信息中
Set<Map.Entry<String, Object>> entries = errorParams.entrySet();
for (Map.Entry<String, Object> entry : entries) {
String key = entry.getKey();
if (!"error".equals(key) && !"error_description".equals(key)) {
Object value = entry.getValue();
ex.addAdditionalInformation(key, value == null ? null : value.toString());
}
}
return ex;
}
}
自定义异常翻译器
模仿框架编写,主要的逻辑还是handleOAuth2Exception
方法,将我们自定义的异常信息返回
import com.example.config.exception.MyOAuth2Exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.DefaultThrowableAnalyzer;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InsufficientScopeException;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.security.web.util.ThrowableAnalyzer;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import java.io.IOException;
/**
* 自定义异常翻译器
* @author 硝酸铜
* @date 2021/9/17
*/
@Slf4j
public class AuthWebResponseExceptionTranslator implements WebResponseExceptionTranslator<OAuth2Exception> {
private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();
@Override
public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(e);
Exception ase = (OAuth2Exception) throwableAnalyzer.getFirstThrowableOfType(OAuth2Exception.class, causeChain);
if (ase != null) {
return handleOAuth2Exception((OAuth2Exception) ase);
}
ase = (AuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class,
causeChain);
if (ase != null) {
return handleOAuth2Exception(new AuthWebResponseExceptionTranslator.UnauthorizedException(e.getMessage(), e));
}
ase = (AccessDeniedException) throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
if (ase instanceof AccessDeniedException) {
return handleOAuth2Exception(new AuthWebResponseExceptionTranslator.ForbiddenException(ase.getMessage(), ase));
}
ase = (HttpRequestMethodNotSupportedException) throwableAnalyzer.getFirstThrowableOfType(
HttpRequestMethodNotSupportedException.class, causeChain);
if (ase instanceof HttpRequestMethodNotSupportedException) {
return handleOAuth2Exception(new AuthWebResponseExceptionTranslator.MethodNotAllowed(ase.getMessage(), ase));
}
return handleOAuth2Exception(new AuthWebResponseExceptionTranslator.ServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), e));
}
private ResponseEntity<OAuth2Exception> handleOAuth2Exception(OAuth2Exception e) throws IOException {
//int status = e.getHttpErrorCode();
// 这里使用http 200的响应码,方便feign调用,feign调用收到400的http 响应码会抛出FeignException$BadRequest异常
// 返回体中有业务相关响应码
int status = 200;
HttpHeaders headers = new HttpHeaders();
headers.set("Cache-Control", "no-store");
headers.set("Pragma", "no-cache");
if (status == HttpStatus.UNAUTHORIZED.value() || (e instanceof InsufficientScopeException)) {
headers.set("WWW-Authenticate", String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, e.getSummary()));
}
//自定义异常信息
MyOAuth2Exception myOAuth2Exception=new MyOAuth2Exception(e.getMessage());
myOAuth2Exception.addAdditionalInformation("code", "401");
myOAuth2Exception.addAdditionalInformation("result", "操作失败");
//将自定义的异常信息放入返回体中
ResponseEntity<OAuth2Exception> response = new ResponseEntity<OAuth2Exception>(myOAuth2Exception, headers,
HttpStatus.valueOf(status));
return response;
}
public void setThrowableAnalyzer(ThrowableAnalyzer throwableAnalyzer) {
this.throwableAnalyzer = throwableAnalyzer;
}
@SuppressWarnings("serial")
private static class ForbiddenException extends OAuth2Exception {
public ForbiddenException(String msg, Throwable t) {
super(msg, t);
}
@Override
public String getOAuth2ErrorCode() {
return "access_denied";
}
@Override
public int getHttpErrorCode() {
return 403;
}
}
@SuppressWarnings("serial")
private static class ServerErrorException extends OAuth2Exception {
public ServerErrorException(String msg, Throwable t) {
super(msg, t);
}
@Override
public String getOAuth2ErrorCode() {
return "server_error";
}
@Override
public int getHttpErrorCode() {
return 500;
}
}
@SuppressWarnings("serial")
private static class UnauthorizedException extends OAuth2Exception {
public UnauthorizedException(String msg, Throwable t) {
super(msg, t);
}
@Override
public String getOAuth2ErrorCode() {
return "unauthorized";
}
@Override
public int getHttpErrorCode() {
return 401;
}
}
@SuppressWarnings("serial")
private static class MethodNotAllowed extends OAuth2Exception {
public MethodNotAllowed(String msg, Throwable t) {
super(msg, t);
}
@Override
public String getOAuth2ErrorCode() {
return "method_not_allowed";
}
@Override
public int getHttpErrorCode() {
return 405;
}
}
}
endpoints配置自定义异常翻译器
在授权服务配置中,配置上自定义异常翻译器,用户处理OAuth2Exception
@EnableAuthorizationServer
@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
...
/**
* 配置授权访问的接入点
* @param endpoints
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
...
endpoints
...
// 自定义异常翻译器
.exceptionTranslator(new AuthWebResponseExceptionTranslator());
}
...
}
测试
授权服务故意输错密码
feign调用故意输错密码:
我们的feign调用返回的是泛型,所以异常信息也能接收到
@PostMapping(value = "/oauth/login")
<T> Result<T> login(@RequestBody LoginReq req);
总结
到这里,SpringSecurityOauth2的技术学习就到一段落了。
到目前为止,我们掌握的技术力可以去面对项目中的权限业务了。但是还是那句话,权限最难的是业务的设计而不是技术。如何设计好一个RBAC系统,这是有大学问的,建议小伙伴们多多去看一些权限相关的业务案例。