Oauth2拦截微服务请求获取token使用负载均衡,解决unKown Host

本文介绍了在微服务架构中,由于OAuth2默认配置无法使用负载均衡获取Token的问题。问题源于在调用认证服务器获取Token时,配置的URL指定了端口而非使用负载均衡。解决方案包括两种:一是自定义Token获取类,覆盖默认的AccessTokenProvider;二是通过动态代理修改OAuth2AccessTokenSupport中的RestTemplate,使其支持负载均衡。这两种方法都能确保在微服务间调用时正确添加负载均衡的Token。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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。

在extract方法里面,oauth会为feign调用的请求上添加token
getToken先从缓存中获取token,如果存在且没失效则直接返回
根据yml配置的oauth地址信息获取token
抽象的获取token方法
具体的获取token实现类
具体获取token的方法
feign调用微服务
被feign.SynchronousMethodHandler#invoke拦截
feign.SynchronousMethodHandler#targetRequest
feign.RequestInterceptor#apply
OAuth2FeignRequestInterceptor#apply
OAuth2FeignRequestInterceptor#extract
OAuth2FeignRequestInterceptor#getToken
OAuth2FeignRequestInterceptor#acquireAccessToken
AccessTokenProvider#obtainAccessToken
AccessTokenProviderChain#obtainNewAccessTokenInternal
ClientCredentialsAccessTokenProvider#obtainAccessToken
OAuth2AccessTokenSupport#retrieveToken
获取token后返回

注:
在这个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;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值