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並重試。