Spring Cloud Feign 使用OAuth2

    Spring Cloud 微服務架構下,服務間的調用採用的是Feign組件,爲了增長服務安全性,server之間互相調用採用OAuth2的client模式。Feign使用http進行服務間的通訊,同時整合了Ribbionspring

使得其具備負載均衡和失敗重試的功能,微服務service-a調用service-b的流程 中大概流程 :安全

 

Feign調用間採用OAuth2驗證的配置架構

1)採用SpringBoot自動加載機制 定義註解繼承@EnableOAuth2Clientmvc

@Import({OAuth2FeignConfigure.class})
@EnableOAuth2Client
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface EnableFeignOAuth2Client {

}

(2)定義配置類OAuth2FeignConfigure負載均衡

  1 public class OAuth2FeignConfigure {
  2     // feign的OAuth2ClientContext
  3     private OAuth2ClientContext feignOAuth2ClientContext =  new DefaultOAuth2ClientContext();
  4 
  5     @Resource
  6     private ClientCredentialsResourceDetails clientCredentialsResourceDetails;
  7 
  8     @Autowired
  9     private ObjectFactory<HttpMessageConverters> messageConverters;
 10 
 11     @Bean
 12     public OAuth2RestTemplate clientCredentialsRestTemplate(){
 13         return new OAuth2RestTemplate(clientCredentialsResourceDetails);
 14     }
 15 
 16     @Bean
 17     public RequestInterceptor oauth2FeignRequestInterceptor(){
 18         return new OAuth2FeignRequestInterceptor(feignOAuth2ClientContext, clientCredentialsResourceDetails);
 19     }
 20 
 21     @Bean
 22     public Logger.Level feignLoggerLevel() {
 23         return Logger.Level.FULL;
 24     }
 25 
 26 
 27     @Bean
 28     public Retryer retry() {
 29         // default Retryer will retry 5 times waiting waiting
 30         // 100 ms per retry with a 1.5* back off multiplier
 31         return new Retryer.Default(100, SECONDS.toMillis(1), 3);
 32     }
 33 
 34 
 35     @Bean
 36     public Decoder feignDecoder() {
 37         return new CustomResponseEntityDecoder(new SpringDecoder(this.messageConverters), feignOAuth2ClientContext);
 38     }
 39 
 40 
 41     /**
 42      *  Http響應成功 可是token失效,須要定製 ResponseEntityDecoder
 43      * @author maxianming
 44      * @date 2018/10/30 9:47
 45      */
 46     class CustomResponseEntityDecoder implements Decoder {
 47         private org.slf4j.Logger log = LoggerFactory.getLogger(CustomResponseEntityDecoder.class);
 48 
 49         private Decoder decoder;
 50 
 51         private OAuth2ClientContext context;
 52 
 53         public CustomResponseEntityDecoder(Decoder decoder, OAuth2ClientContext context) {
 54             this.decoder = decoder;
 55             this.context = context;
 56         }
 57 
 58         @Override
 59         public Object decode(final Response response, Type type) throws IOException, FeignException {
 60             if (log.isDebugEnabled()) {
 61                 log.debug("feign decode type:{},reponse:{}", type, response.body());
 62             }
 63             if (isParameterizeHttpEntity(type)) {
 64                 type = ((ParameterizedType) type).getActualTypeArguments()[0];
 65                 Object decodedObject = decoder.decode(response, type);
 66                 return createResponse(decodedObject, response);
 67             }
 68             else if (isHttpEntity(type)) {
 69                 return createResponse(null, response);
 70             }
 71             else {
 72                 // custom ResponseEntityDecoder if token is valid then go to errorDecoder
 73                 String body = Util.toString(response.body().asReader());
 74                 if (body.contains(ServerConstant.INVALID_TOKEN.getCode())) {
 75                     clearTokenAndRetry(response, body);
 76                 }
 77                 return decoder.decode(response, type);
 78             }
 79         }
 80         
 81         /**
 82          * token失效 則將token設置爲null 而後重試
 83          * @author maxianming
 84          * @param
 85          * @return 
 86          * @date 2018/10/30 10:05
 87          */
 88         private void clearTokenAndRetry(Response response, String body) throws FeignException {
 89             log.error("接收到Feign請求資源響應,響應內容:{}",body);
 90             context.setAccessToken(null);
 91             throw new RetryableException("access_token過時,即將進行重試", new Date());
 92         }
 93 
 94         private boolean isParameterizeHttpEntity(Type type) {
 95             if (type instanceof ParameterizedType) {
 96                 return isHttpEntity(((ParameterizedType) type).getRawType());
 97             }
 98             return false;
 99         }
100 
101         private boolean isHttpEntity(Type type) {
102             if (type instanceof Class) {
103                 Class c = (Class) type;
104                 return HttpEntity.class.isAssignableFrom(c);
105             }
106             return false;
107         }
108 
109         @SuppressWarnings("unchecked")
110         private <T> ResponseEntity<T> createResponse(Object instance, Response response) {
111 
112             MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
113             for (String key : response.headers().keySet()) {
114                 headers.put(key, new LinkedList<>(response.headers().get(key)));
115             }
116             return new ResponseEntity<>((T) instance, headers, org.springframework.http.HttpStatus.valueOf(response
117                     .status()));
118         }
119     }
120 
121 
122 
123     @Bean
124     public ErrorDecoder errorDecoder() {
125         return new RestClientErrorDecoder(feignOAuth2ClientContext);
126     }
127 
128     /**
129      *  Feign調用HTTP返回響應碼錯誤時候,定製錯誤的解碼
130      * @author maxianming
131      * @date 2018/10/30 9:45
132      */
133     class RestClientErrorDecoder implements ErrorDecoder {
134         private org.slf4j.Logger logger = LoggerFactory.getLogger(RestClientErrorDecoder.class);
135 
136         private OAuth2ClientContext context;
137 
138         RestClientErrorDecoder(OAuth2ClientContext context) {
139             this.context = context;
140         }
141 
142         public Exception decode(String methodKey, Response response) {
143             logger.error("Feign調用異常,異常methodKey:{}, token:{}, response:{}", methodKey, context.getAccessToken(), response.body());
144             if (HttpStatus.SC_UNAUTHORIZED == response.status()) {
145                 logger.error("接收到Feign請求資源響應401,access_token已通過期,重置access_token爲null待從新獲取。");
146                 context.setAccessToken(null);
147                 return new RetryableException("疑似access_token過時,即將進行重試", new Date());
148             }
149             return errorStatus(methodKey, response);
150         }
151     }
152 
153 
154 }

一、使用ClientCredentialsResourceDetails (即client_id、 client-secret、user-info-uri等信息配置在配置中心)初始化OAuth2RestTemplate,用戶請求建立token時候驗證基本信息ide

二、主要定義了攔截器初始化了OAuth2FeignRequestInterceptor ,使得Feign進行RestTemplate調用的請求前進行token攔截。 若是不存在token則須要auth-server中獲取token微服務

三、注意上下文對象OAuth2ClientContext創建後不放在Bean容器中,主要放在Bean容器,Spring mvc的前置處理器, 會複製token到OAuth2ClientContext中, 致使用戶的token會覆蓋服務間的token當不一樣         token間的權限不一樣時,驗證會不經過。this

四、從新定義了 Decoder 即,RestTemple http調用的響應進行解碼, 因爲token失效時進行了擴展,spa

      默認狀況下:token失效會返回401錯誤的http響應,致使進入ErrorDecoder流程,在ErrorDecoder中若是token過時,則進行除掉token,Feign重試。debug

      擴展後:返回的是token失效的錯誤碼,因此會走Decoder流程,因此對ResponseEntityDecoder進行了擴展,若是無效token錯誤碼,則清空token並重試。

相關文章
相關標籤/搜索