文章目錄
- OpenFeign通路需要OAuth2授權的服務
-
- 概述
- 示例
-
- OAuth2.0相關配置
-
- 引入依賴
- 配置application.yml
- 配置資源伺服器
- OAuth2FeignConfiguration
-
- 引入依賴
- FeignClient使用
- 編寫OAuth2FeignClient
- ==編寫OAuth2FeignConfiguration(重點)==
- OAuth2FeignConfiguration相關說明
- 擴充
- 參考
OpenFeign通路需要OAuth2授權的服務
概述
Spring Cloud 微服務架構下使用feign元件進行服務間的調用,該元件使用http協定進行服務間的通信,同時整合了Ribbion使其具有負載均衡和失敗重試的功能,微服務service-a調用需要授權的service-b的流程中大概流程 :
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-gf1QUs9i-1618850710274)(https://img2018.cnblogs.com/blog/733995/201810/733995-20181031151136077-1818990556.png “微服務service-a調用需要授權的service-b流程圖”)]
随着微服務安全性的增強,需要攜帶token才能通路其API,然而feign元件預設并不會将 token 放到 Header 中,那麼如何使用OpenFeign實作自動設定授權資訊并通路需要OAuth2授權的服務呢?
本文重點講述如何通過
RequestInterceptor
實作自動設定授權資訊,并通路需要OAuth2的client模式授權的服務。需要重點了解下面兩點:
- OAuth2.0配置
- OAuth2FeignRequestInterceptor
本文依賴:
- spring-boot-starter-parent:2.4.2
- spring-cloud-starter-openfeign:3.0.0
- spring-cloud-starter-oauth2:2.2.4.RELEASE
示例
OAuth2.0相關配置
引入依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.4.RELEASE</version>
</dependency>
配置application.yml
auth.service:: http://localhost:8080
security:
oauth2:
client:
client-id: car-client
client-secret: 123456
grant-type: client_credentials
access-token-uri: ${auth.service}/oauth/token #請求令牌的位址
scope:
- all
resource:
jwt:
key-uri: ${auth.service}/oauth/token_key
user-info-uri: ${auth.service}/api/sso/user/me
配置資源伺服器
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatchers().antMatchers("/**")
.and().authorizeRequests()
.antMatchers("/**").permitAll()
.anyRequest().authenticated();
}
}
OAuth2FeignConfiguration
引入依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>3.0.2</version>
</dependency>
FeignClient使用
@Resource
private OAuth2FeignClient oAuth2FeignClient;
...
String vo = oAuth2FeignClient.getMemberInfo();
編寫OAuth2FeignClient
oauth2.api.url: http://localhost:8081
@FeignClient(url = "${oauth2.api.url}", name = "oauth2FeignClient", configuration = OAuth2FeignConfiguration.class)
public interface OAuth2FeignClient {
@PostMapping("/car/info")
String getCarInfo();
}
編寫OAuth2FeignConfiguration(重點)
public class OAuth2FeignConfiguration {
/** feign的OAuth2ClientContext */
private final OAuth2ClientContext feignOAuth2ClientContext = new DefaultOAuth2ClientContext();
@Resource
private ClientCredentialsResourceDetails clientCredentialsResourceDetails;
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
@Bean
public OAuth2RestTemplate clientCredentialsRestTemplate() {
return new OAuth2RestTemplate(clientCredentialsResourceDetails);
}
@Bean
public RequestInterceptor oauth2FeignRequestInterceptor() {
return new OAuth2FeignRequestInterceptor(feignOAuth2ClientContext, clientCredentialsResourceDetails);
}
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
@Bean
public Retryer retry() {
// default Retryer will retry 5 times waiting waiting
// 100 ms per retry with a 1.5* back off multiplier
return new Retryer.Default(100, SECONDS.toMillis(1), 3);
}
@Bean
public Decoder feignDecoder() {
return new CustomResponseEntityDecoder(new SpringDecoder(this.messageConverters), feignOAuth2ClientContext);
}
/**
* Http響應成功 但是token失效,需要定制 ResponseEntityDecoder
* @author maxianming
* @date 2018/10/30 9:47
*/
class CustomResponseEntityDecoder implements Decoder {
private org.slf4j.Logger log = LoggerFactory.getLogger(CustomResponseEntityDecoder.class);
private Decoder decoder;
private OAuth2ClientContext context;
public CustomResponseEntityDecoder(Decoder decoder, OAuth2ClientContext context) {
this.decoder = decoder;
this.context = context;
}
@Override
public Object decode(final Response response, Type type) throws IOException, FeignException {
if (log.isDebugEnabled()) {
log.debug("feign decode type:{},reponse:{}", type, response.body());
}
if (isParameterizeHttpEntity(type)) {
type = ((ParameterizedType) type).getActualTypeArguments()[0];
Object decodedObject = decoder.decode(response, type);
return createResponse(decodedObject, response);
} else if (isHttpEntity(type)) {
return createResponse(null, response);
} else {
// custom ResponseEntityDecoder if token is valid then go to errorDecoder
String body = Util.toString(response.body().asReader(Util.UTF_8));
if (body.contains("401 Unauthorized")) {
clearTokenAndRetry(response, body);
}
return decoder.decode(response, type);
}
}
/**
* token失效 則将token設定為null 然後重試
* @param response response
* @param body body
* @author maxianming
* @date 2018/10/30 10:05
*/
private void clearTokenAndRetry(Response response, String body) throws FeignException {
log.error("接收到Feign請求資源響應,響應内容:{}", body);
context.setAccessToken(null);
throw new RetryableException(
response.status(),
"access_token過期,即将進行重試",
response.request().httpMethod(),
new Date(),
response.request());
}
private boolean isParameterizeHttpEntity(Type type) {
if (type instanceof ParameterizedType) {
return isHttpEntity(((ParameterizedType) type).getRawType());
}
return false;
}
private boolean isHttpEntity(Type type) {
if (type instanceof Class) {
Class c = (Class) type;
return HttpEntity.class.isAssignableFrom(c);
}
return false;
}
@SuppressWarnings("unchecked")
private <T> ResponseEntity<T> createResponse(Object instance, Response response) {
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
for (String key : response.headers().keySet()) {
headers.put(key, new LinkedList<>(response.headers().get(key)));
}
return new ResponseEntity<>((T) instance, headers, HttpStatus.valueOf(response.status()));
}
}
@Bean
public ErrorDecoder errorDecoder() {
return new RestClientErrorDecoder(feignOAuth2ClientContext);
}
/**
* Feign調用HTTP傳回響應碼錯誤時候,定制錯誤的解碼
* @author liudong
* @date 2018/10/30 9:45
*/
class RestClientErrorDecoder implements ErrorDecoder {
private org.slf4j.Logger logger = LoggerFactory.getLogger(RestClientErrorDecoder.class);
private OAuth2ClientContext context;
RestClientErrorDecoder(OAuth2ClientContext context) {
this.context = context;
}
@Override
public Exception decode(String methodKey, Response response) {
FeignException exception = errorStatus(methodKey, response);
logger.error("Feign調用異常,異常methodKey:{}, token:{}, response:{}", methodKey, context.getAccessToken(), response.body());
if (HttpStatus.UNAUTHORIZED.value() == response.status()) {
logger.error("接收到Feign請求資源響應401,access_token已經過期,重置access_token為null待重新擷取。");
context.setAccessToken(null);
return new RetryableException(
response.status(),
"疑似access_token過期,即将進行重試",
response.request().httpMethod(),
exception,
new Date(),
response.request());
}
return exception;
}
}
}
OAuth2FeignConfiguration相關說明
- 使用
(client_id、client-secret、jwt.key-uri/user-info-uri等資訊配置在配置中心)初始化ClientCredentialsResourceDetails
,使用者請求建立token時候驗證基本資訊;OAuth2RestTemplate
- 主要定義了攔截器初始化了
,使得Feign進行OAuth2FeignRequestInterceptor
調用請求前進行token攔截。如果不存在token則需要從auth-server中擷取token;RestTemplate
- 注意上下文對象
建立後不放在Bean容器中:由于Spring mvc的前置處理器, 會複制使用者的token到OAuth2ClientContext中,如果放在Bean容器中,使用者的token會覆寫服務間的token,當兩個token的權限不同時,将導緻驗證不通過;OAuth2ClientContext
- 重新定義了
,對RestTemple http調用的響應進行了解碼,對token失效的情況進行了擴充:Decoder
- 預設情況下:對于由于token失效傳回401錯誤的http響應,導緻進入
的情況,在ErrorDecoder
中進行清空token操作,并傳回ErrorDecoder
,讓Feign重試。RetryableException
- 擴充後:對于接口200響應token失效的錯誤碼的情況,将會走
流程,是以對Decoder
進行了擴充,如果響應無效token錯誤碼,則清空token并重試。ResponseEntityDecoder
- 預設情況下:對于由于token失效傳回401錯誤的http響應,導緻進入
擴充
-
copyOAuth2FeignRequestInterceptor
的擷取token内容, 後者實作了擷取token并存入context未逾時時不會再次請求授權伺服器,減輕了授權伺服器的開銷OAuth2RestTemplate
-
可以拓展為其他3種授權模式的Details, 有興趣的請移步至ClientCredentialsResourceDetails
的源碼OAuth2ProtectedResourceDetails
- Bean容器中的
的token與服務間調用所需的token權限不同; 或者目前上下文中沒有token,但後者調用需要token(Spring mvc的前置處理器, 會複制token到OAuth2ClientContext中); 這兩種情況均可以建立不放入Bean容器中的OAuth2ClientContext
OAuth2ClientContext
- 如果Bean容器中的
的token與服務間調用所需的token權限相同, 可以注入Bean容器中的OAuth2ClientContext
; 也可以參考SpringCloud 中 Feign 調用添加 Oauth2 Authorization Header, 或者 feign之間傳遞oauth2-token的問題和解決 來實作, 其擷取token的核心邏輯可以參考源碼OAuth2ClientContext
;org.springframework.cloud.commons.security.AccessTokenContextRelay
- (未解決)
在OAuth2RestTemplate
擔任什麼角色? 有什麼作用?OAuth2FeignConfiguration
- (未解決)啟動類不能配置
, 否則無法啟動項目, 有木有大佬知道原因? 個人猜測和@EnableOAuth2Client
有關系AccessTokenContextRelay
- (未解決)該文檔學會了
的用法, 那麼OAuth2FeignRequestInterceptor
又用于什麼場景呢?BasicAuthRequestInterceptor
- (未解決)嘗試使用
的設計思路實作ResponseMappingDecoder
CustomResponseEntityDecoder
- (未解決)
于2020年8月1日釋出了spring-cloud-starter-oauth2
版本後一直沒有更新, 如果其不維護又該怎麼辦呢? 有木有其他實作方式呢?2.2.4.RELEASE
- (未解決)
與spring-boot-starter-oauth2-client
剛剛釋出了spring-boot-starter-oauth2-resource-server
版本, 其一直在更新, 是否可以用來實作OAuth2配置部分? 可以參考官方文檔進行評估(前者支援配置多個APP的client資訊并分别擷取授權)2.4.5
參考
- 優秀文檔-浮生半日
- 參考文檔
- 本文源碼