系列導航
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系統,這是有大學問的,建議小伙伴們多多去看一些權限相關的業務案例。