Spring Cloud Feign 使用OAuth2

    Spring Cloud 微服务架构下,服务间的调用采用的是Feign组件,为了增加服务安全性,server之间互相调用采用OAuth2的client模式。Feign使用http进行服务间的通信,同时整合了Ribbion

使得其具有负载均衡和失败重试的功能,微服务service-a调用service-b的流程 中大概流程 :

 

Feign调用间采用OAuth2验证的配置

1)采用SpringBoot自动加载机制 定义注解继承@EnableOAuth2Client

@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 }

1、使用ClientCredentialsResourceDetails (即client_id、 client-secret、user-info-uri等信息配置在配置中心)初始化OAuth2RestTemplate,用户请求创建token时候验证基本信息

2、主要定义了拦截器初始化了OAuth2FeignRequestInterceptor ,使得Feign进行RestTemplate调用的请求前进行token拦截。 如果不存在token则需要auth-server中获取token

3、注意上下文对象OAuth2ClientContext建立后不放在Bean容器中,主要放在Bean容器,Spring mvc的前置处理器, 会复制token到OAuth2ClientContext中, 导致用户的token会覆盖服务间的token当不同         token间的权限不同时,验证会不通过。

4、重新定义了 Decoder 即,RestTemple http调用的响应进行解码, 由于token失效时进行了扩展,

      默认情况下:token失效会返回401错误的http响应,导致进入ErrorDecoder流程,在ErrorDecoder中如果token过期,则进行除掉token,Feign重试。

      扩展后:返回的是token失效的错误码,所以会走Decoder流程,所以对ResponseEntityDecoder进行了扩展,如果无效token错误码,则清空token并重试。


     

 

  

posted @ 2018-10-31 15:47  浮生若云  阅读(18445)  评论(2编辑  收藏  举报