Oauth2拦截微服务请求获取token使用负载均衡
一,问题引出
微服务之间相互调用,使用oauth默认的配置,获取token的方式为grant-type: client_credentials,yml配置:access-token-uri: http://cloud-auth-dev.crpt-dev:8001/oauth/token,获取认证的url必须指定端口,不能使用负载均衡的方式,否则会报错,unKown Host xxx;
yml配置截图:
security:
oauth2:
client:
client-id: client
client-secret: sdecsdg
access-token-uri: http://clouds-auth-dev.crpt-dev:8001/oauth/token
user-authorization-uri: http://clouds-auth/oauth/authorize
grant-type: client_credentials
scope: app
二,问题原因
在微服务A调用微服务B之前,oauth2会拦截A调用B的请求,并为其添加A从认证服务器获取的token,然后在发起A调用B。
注:
在这个OAuth2AccessTokenSupport#retrieveToken方法内获取的RestTemplate为, new RestTemplate(),因为是new出来的,不带@LoadBalanced注解,所有不支持访问域名。因此想要使用负载均衡,必须重新获取token的方法。也就是自定义具体的token获取类,或者重写retrieveToken中获取RestTemplate的方法。
三,解决方案:
方案一,重写获取token的方法,即重写obtainAccessToken方法,自定义类,继承ClientCredentialsAccessTokenProvider,并重写其获取token的方法obtainAccessToken。并在Spring容器初始化,创建OAuth2FeignRequestInterceptor类时,将自定义的对象添加给它。
自定义获取token代码:
@Slf4j
@Component
public class JobClientAccessTokenProvider extends ClientCredentialsAccessTokenProvider {
@Autowired
private RestTemplate restTemplate;
@Autowired
private SecurityProperties securityProperties;
@Override
public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest parameters) throws UserRedirectRequiredException, UserApprovalRequiredException, AccessDeniedException {
// 注意url上要拼接?grant_type=client_credentials&scope=app,不然oauth会报,找不到grant_type
String url = securityProperties.getAccessTokenUri() + "?grant_type=" + securityProperties.getGrantType() + "&scope=" + securityProperties.getScope();
String format = "";
try {
// String.format的代码出自oauth源码,类:OAuth2FeignRequestInterceptor -> DefaultClientAuthenticationHandler
format = String.format(
"Basic %s",
new String(Base64.getEncoder().encode(String.format("%s:%s", securityProperties.getClientId(),
securityProperties.getClientSecret()).getBytes("UTF-8")), "UTF-8"));
System.out.println(format);
} catch (Exception e) {
log.error("===== 获取Token失败,转换异常", e);
throw new RuntimeException(e);
}
HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaders.AUTHORIZATION, format);
HttpEntity<String> request = new HttpEntity<String>(headers);
ResponseEntity<OAuth2AccessToken> response = restTemplate.exchange(url, HttpMethod.POST, request, OAuth2AccessToken.class);
return response.getBody();
}
配置类:
@Data
@Component
@RefreshScope
public class SecurityProperties {
@Value("${security.oauth2.client.client-id}")
private String clientId;
@Value("${security.oauth2.client.client-secret}")
private String clientSecret;
@Value("${security.oauth2.client.accessTokenUri}")
private String accessTokenUri;
@Value("${security.oauth2.client.grant-type}")
private String grantType;
@Value("${security.oauth2.client.scope}")
private String scope;
}
Spring容器创建OAuth2FeignRequestInterceptor后设置其属性:
@Component
public class JobAccessTokenBeanPostProcessor implements BeanPostProcessor {
@Autowired
private JobClientAccessTokenProvider jobClientAccessTokenProvider;
// org.springframework.cloud.security.oauth2.client.feign.OAuth2FeignRequestInterceptor
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if("oauth2FeignRequestInterceptor".equals(beanName)){
OAuth2FeignRequestInterceptor oauthInterceptor = (OAuth2FeignRequestInterceptor) bean;
oauthInterceptor.setAccessTokenProvider(jobClientAccessTokenProvider);
}
return bean;
}
注意:添加会覆盖源码中默认的4个AccessTokenProvider,如果不想覆盖,就按照源码的方式重写添加一便就可以。
源码设置默认的AccessTokenProvider代码:OAuth2FeignRequestInterceptor中
private AccessTokenProvider accessTokenProvider = new AccessTokenProviderChain(
Arrays.<AccessTokenProvider>asList(new AuthorizationCodeAccessTokenProvider(),
new ImplicitAccessTokenProvider(),
new ResourceOwnerPasswordAccessTokenProvider(),
new ClientCredentialsAccessTokenProvider()));
方案二,为OAuth2AccessTokenSupport的getRestTemplate()方法添加动态代理,获取我们添加了@LoadBalanced的RestTemplate对象。
源码有4个获取token 的provider,都直接替换,getRestTemplate()方法。
动态代理类:
@Component
public class ProxyTokenProviderGetRestTemplate implements MethodInterceptor {
@Autowired
private RestTemplate restTemplate;
public Object getProxyInstance(Object tokenProvider) {
Enhancer enhance = new Enhancer();
enhance.setSuperclass(tokenProvider.getClass());
enhance.setCallback(this);
enhance.setClassLoader(this.getClass().getClassLoader());
return enhance.create();
}
// proxy 是代理对象
@Override
public Object intercept(Object proxy, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
if ("getRestTemplate".equals(method.getName())) {
Class<?> superclass = proxy.getClass().getSuperclass().getSuperclass();
Field templateFiled = superclass.getDeclaredField("restTemplate");
templateFiled.setAccessible(true);
templateFiled.set(proxy, restTemplate);
Field messageConverters = superclass.getDeclaredField("messageConverters");
messageConverters.setAccessible(true);
if (messageConverters.get(proxy) == null) {
addMessageConverters(proxy);
}
return restTemplate;
}
return methodProxy.invokeSuper(proxy, objects);
}
private void addMessageConverters(Object proxy) {
OAuth2AccessTokenSupport tokenSupport;
if (proxy instanceof AuthorizationCodeAccessTokenProvider) {
tokenSupport = ((AuthorizationCodeAccessTokenProvider) proxy);
} else if (proxy instanceof ImplicitAccessTokenProvider) {
tokenSupport = ((ImplicitAccessTokenProvider) proxy);
} else if (proxy instanceof ResourceOwnerPasswordAccessTokenProvider) {
tokenSupport = ((ResourceOwnerPasswordAccessTokenProvider) proxy);
} else {
tokenSupport = ((ClientCredentialsAccessTokenProvider) proxy);
}
tokenSupport.setMessageConverters(new RestTemplate().getMessageConverters());
}
}
Spring容器创建OAuth2FeignRequestInterceptor后设置其属性:
@Component
public class JobAccessTokenBeanPostProcessor implements BeanPostProcessor {
@Autowired
private ProxyTokenProviderGetRestTemplate getRestTemplate;
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if ("oauth2FeignRequestInterceptor".equals(beanName)) {
OAuth2FeignRequestInterceptor oauthInterceptor = (OAuth2FeignRequestInterceptor) bean;
AuthorizationCodeAccessTokenProvider authorizationTokenProviderProxy = (AuthorizationCodeAccessTokenProvider) getRestTemplate.getProxyInstance(new AuthorizationCodeAccessTokenProvider());
ImplicitAccessTokenProvider implicitAccessTokenProviderProxy = (ImplicitAccessTokenProvider) getRestTemplate.getProxyInstance(new ImplicitAccessTokenProvider());
ResourceOwnerPasswordAccessTokenProvider resourceTokenProviderProxy = (ResourceOwnerPasswordAccessTokenProvider) getRestTemplate.getProxyInstance(new ResourceOwnerPasswordAccessTokenProvider());
ClientCredentialsAccessTokenProvider clientTokenProviderProxy = (ClientCredentialsAccessTokenProvider) getRestTemplate.getProxyInstance(new ClientCredentialsAccessTokenProvider());
AccessTokenProvider accessTokenProvider = new AccessTokenProviderChain(
Arrays.<AccessTokenProvider>asList(authorizationTokenProviderProxy,
implicitAccessTokenProviderProxy,
resourceTokenProviderProxy,
clientTokenProviderProxy));
oauthInterceptor.setAccessTokenProvider(accessTokenProvider);
}
return bean;
}
}