天天看點

【feign】OpenFeign通路需要OAuth2授權的服務OpenFeign通路需要OAuth2授權的服務

文章目錄

  • 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相關說明

  1. 使用

    ClientCredentialsResourceDetails

    (client_id、client-secret、jwt.key-uri/user-info-uri等資訊配置在配置中心)初始化

    OAuth2RestTemplate

    ,使用者請求建立token時候驗證基本資訊;
  2. 主要定義了攔截器初始化了

    OAuth2FeignRequestInterceptor

    ,使得Feign進行

    RestTemplate

    調用請求前進行token攔截。如果不存在token則需要從auth-server中擷取token;
  3. 注意上下文對象

    OAuth2ClientContext

    建立後不放在Bean容器中:由于Spring mvc的前置處理器, 會複制使用者的token到OAuth2ClientContext中,如果放在Bean容器中,使用者的token會覆寫服務間的token,當兩個token的權限不同時,将導緻驗證不通過;
  4. 重新定義了

    Decoder

    ,對RestTemple http調用的響應進行了解碼,對token失效的情況進行了擴充:
    1. 預設情況下:對于由于token失效傳回401錯誤的http響應,導緻進入

      ErrorDecoder

      的情況,在

      ErrorDecoder

      中進行清空token操作,并傳回

      RetryableException

      ,讓Feign重試。
    2. 擴充後:對于接口200響應token失效的錯誤碼的情況,将會走

      Decoder

      流程,是以對

      ResponseEntityDecoder

      進行了擴充,如果響應無效token錯誤碼,則清空token并重試。

擴充

  • OAuth2FeignRequestInterceptor

    copy

    OAuth2RestTemplate

    的擷取token内容, 後者實作了擷取token并存入context未逾時時不會再次請求授權伺服器,減輕了授權伺服器的開銷
  • ClientCredentialsResourceDetails

    可以拓展為其他3種授權模式的Details, 有興趣的請移步至

    OAuth2ProtectedResourceDetails

    的源碼
  • Bean容器中的

    OAuth2ClientContext

    的token與服務間調用所需的token權限不同; 或者目前上下文中沒有token,但後者調用需要token(Spring mvc的前置處理器, 會複制token到OAuth2ClientContext中); 這兩種情況均可以建立不放入Bean容器中的

    OAuth2ClientContext

  • 如果Bean容器中的

    OAuth2ClientContext

    的token與服務間調用所需的token權限相同, 可以注入Bean容器中的

    OAuth2ClientContext

    ; 也可以參考SpringCloud 中 Feign 調用添加 Oauth2 Authorization Header, 或者 feign之間傳遞oauth2-token的問題和解決 來實作, 其擷取token的核心邏輯可以參考源碼

    org.springframework.cloud.commons.security.AccessTokenContextRelay

    ;
  • (未解決)

    OAuth2RestTemplate

    OAuth2FeignConfiguration

    擔任什麼角色? 有什麼作用?
  • (未解決)啟動類不能配置

    @EnableOAuth2Client

    , 否則無法啟動項目, 有木有大佬知道原因? 個人猜測和

    AccessTokenContextRelay

    有關系
  • (未解決)該文檔學會了

    OAuth2FeignRequestInterceptor

    的用法, 那麼

    BasicAuthRequestInterceptor

    又用于什麼場景呢?
  • (未解決)嘗試使用

    ResponseMappingDecoder

    的設計思路實作

    CustomResponseEntityDecoder

  • (未解決)

    spring-cloud-starter-oauth2

    于2020年8月1日釋出了

    2.2.4.RELEASE

    版本後一直沒有更新, 如果其不維護又該怎麼辦呢? 有木有其他實作方式呢?
  • (未解決)

    spring-boot-starter-oauth2-client

    spring-boot-starter-oauth2-resource-server

    剛剛釋出了

    2.4.5

    版本, 其一直在更新, 是否可以用來實作OAuth2配置部分? 可以參考官方文檔進行評估(前者支援配置多個APP的client資訊并分别擷取授權)

參考

  • 優秀文檔-浮生半日
  • 參考文檔
  • 本文源碼

繼續閱讀