SpringCloud微服务中Feign如何传递用户Token,并保证多线程环境也可适用?

虽然可以在异步调用时设置 RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true); 可以实现请求头透传,但是每次调用都需要加上这一句,实现上还略显麻烦。

虽然可以在异步调用时设置 RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true); 可以实现请求头透传,但是每次调用都需要加上这一句,实现上还略显麻烦。

大家好,我是飘渺。

在上一篇文章中,我们解决了网关层认证后向后端服务传递用户信息的问题。今天我们来解决另外一个问题:如何在 OpenFeign 中传递 Token,并且保证多线程情况下也能适用。

这是DDD&微服务系列文章的第34篇,欢迎持续关注!

为了方便演示,首先定义一个接口,在接口中通过 Feign 调用其他服务:

@Operation(summary = "用户测试接口")  
@GetMapping("/api/pd/customer/info")  
public String info() {  
      
    String currentUser = UserContextHolder.getInstance().getCurrentUser();  
      
    log.info("feign调用方获取当前登录用户:" + currentUser);  
    
 //通过feign调用远程服务
    String info = experimentClient.info();  
  
    log.info("远程获取用户:" + info);
    return currentUser;  
}

然后在远程接口中通过上文定义的UserContextHolder对象获取用户信息:

@GetMapping("/api/pd/experiment/info")  
public String userInfo() {  
  
    String currentUser = UserContextHolder.getInstance().getCurrentUser();  
  
    log.info("feign被调用方获取userToken : {} ",currentUser);  
  
    return currentUser == null ? "" : currentUser;  
}

图片图片

通过调用结果可知,当使用OpenFeign调用远程服务时,接口是无法获取到用户 ID 的。

常规解决办法

在使用OpenFeign请求其他服务接口时,默认不携带header信息,这样就导致无法携带登录用户信息。常规情况下,我们只需要在使用 OpenFeign 调用时先从 Header 获取 Token 信息,放入新请求即可,在项目中可以定义一个OpenFeign的拦截器来实现此功能,代码如下所示:

public class FeignRequestConfiguration {  
  
    @Bean  
    public RequestInterceptor requestInterceptor(){  
        return new RequestInterceptor() {  
            @Override  
            public void apply(RequestTemplate template) {  
                RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();  
                ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;  
  
                // 当主线程的请求执行完毕后,Servlet容器会被销毁当前的Servlet,因此在这里需要做判空  
                if (attributes != null) {  
                    HttpServletRequest request = attributes.getRequest();  
  
                    // 获取userId 并传递 userId                    
                    String userId = request.getHeader(CommonConstant.X_CLIENT_TOKEN);  
                    if (StringUtils.hasText(userId)) {  
                        template.header(CommonConstant.X_CLIENT_TOKEN, userId);  
                    }  
                }  
            }  
        };  
    }  
}

经过上述配置以后再次调用即可在 Feign 接口中也获取到用户ID,如下图所示:

图片图片

异步调用

上面是单线程的情况,假如我们在当前线程中又开启了子线程去进行 Feign 调用,那么是无法从RequestContextHolder获取到 Header 的。测试代码如下:

public String info() {  
      
    String currentUser = UserContextHolder.getInstance().getCurrentUser();  
      
    log.info("feign调用方获取当前登录用户:" + currentUser);  
  
    CompletableFuture<String> infoFuture = CompletableFuture.supplyAsync(experimentClient::info,executor);

    String info = "";  
    try{  
        info = infoFuture.get();  
    } catch (Exception e) {  
        e.printStackTrace();  
        throw new RuntimeException(e);  
    }  
  
    log.info("远程获取用户:" + info);  
  
    return currentUser;  
}

在上述代码中,通过 CompletableFuture 开启异步线程去调用 experimentClient ,可以发现此时无法获取到用户信息,效果如下所示:

图片图片

出现上述问题的原因是,RequestContextHolder.getRequestAttributes()方法里面使用的一个ThreadLocal,默认不是线程共享的,源码如下:

public static RequestAttributes getRequestAttributes() {  
    RequestAttributes attributes = requestAttributesHolder.get();  
    if (attributes == null) {  
       attributes = inheritableRequestAttributesHolder.get();  
    }  
    return attributes;  
}

所以主线程调用子线程时,无法获取到主线程请求里面的RequestAttributes。

解决办法

原因已经清楚了,继续观察RequestContextHolder.getRequestAttributes()方法源码,注意到如果当前线程拿不到RequestAttributes,它会从inheritableRequestAttributesHolder里面拿,再仔细观察发现源码设置RequestAttributes到ThreadLocal的时候有这样一个重载方法。

/**
 * 给当前线程绑定属性
 * @param inheritable 是否要将属性暴露给子线程
 */
public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) {  
    ......
}

这看起来符合我们的要求,只需要在主线程调用其他线程前将 RequestAttributes 对象设置为子线程共享,就能把 Header 等信息传递下去。

所以,在异步调用 Feign 接口时添加如下代码即可:

RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(),true);
CompletableFuture<String> infoFuture = CompletableFuture.supplyAsync(experimentClient::info,executor);
......

再次执行发现,是可以获取到 userId 的。

这里使用CompletableFuture异步调用时需要使用自定义线程池,而不能使用默认线程池ForkJoinPool,这是为什么呢?

最佳解决方案

虽然可以在异步调用时设置RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);可以实现请求头透传,但是每次调用都需要加上这一句,实现上还略显麻烦。

并且我们知道了获取不到请求头的原因是子线程无法获取主线程的 header 属性,那么我们只需要定义一个数据结构,使用InheritableThreadLocal在内存中保存一份 header 属性即可。在上篇文章中通过网关进行 UserID 透传时我们是使用 ThreadLocal 保存数据,现在只需要将其换成InheritableThreadLocal,同时在RequestInterceptor#apply()方法中不再通过请求头获取而是直接从InheritableThreadLocal中获取数据。

实现过程如下:

1、重命名并修改数据结构:

首先,将UserContextHolder重命名为RequestHeaderHolder,同时使用InheritableThreadLocal替换ThreadLocal,以便子线程也能获取数据。

public class RequestHeaderHolder {
    private final ThreadLocal<Map<String,String>> REQUEST_HEADER_HOLDER;

    //使用InheritableThreadLocal,使得共享变量可被子线程继承
    private RequestHeaderHolder() {
        this.REQUEST_HEADER_HOLDER = new InheritableThreadLocal<>() {
            @Override
            protected Map<String, String> initialValue() {
                return new HashMap<>();
            }
        };
    }
  
   public String getCurrentUser(){
        return this.REQUEST_HEADER_HOLDER.get().get(CommonConstant.X_CLIENT_TOKEN);
   }
  ......
}

2、修改请求拦截器:

将请求拦截器UserTokenInterceptor重命名为RequestHeaderInterceptor,并将请求头放入RequestHeaderHolder中。

@Slf4j
public class RequestHeaderInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception {

        Enumeration<String> headerNames = request.getHeaderNames();
        RequestHeaderHolder requestHeaderHolder = RequestHeaderHolder.getInstance();

        //重新设置请求头
        while (headerNames.hasMoreElements()){
            String key = headerNames.nextElement();
            requestHeaderHolder.set(key,request.getHeader(key));
        }
        return true;
    }
    
   ......
}

3、修改 Feign 配置类:在FeignRequestConfiguration中不再从RequestContextHolder获取数据,而是从RequestHeaderHolder获取数据。

@Slf4j
public class FeignRequestConfiguration {

    @Bean
    public RequestInterceptor requestInterceptor(){
        return template -> {
            Map<String, String> headerMap = RequestHeaderHolder.getInstance().get();
            if(headerMap != null){
                headerMap.forEach((key, value) -> {                   
                    template.header(key, value);
                });
            }
        };
    }
}

通过上面的改造,不管是同步调用还是子线程异步调用都可以直接通过RequestHeaderHolder.getInstance().getCurrentUser();获取用户信息,并且调用方无须做任何改动。

©本文为清一色官方代发,观点仅代表作者本人,与清一色无关。清一色对文中陈述、观点判断保持中立,不对所包含内容的准确性、可靠性或完整性提供任何明示或暗示的保证。本文不作为投资理财建议,请读者仅作参考,并请自行承担全部责任。文中部分文字/图片/视频/音频等来源于网络,如侵犯到著作权人的权利,请与我们联系(微信/QQ:1074760229)。转载请注明出处:清一色财经

(0)
打赏 微信扫码打赏 微信扫码打赏 支付宝扫码打赏 支付宝扫码打赏
清一色的头像清一色管理团队
上一篇 2024年3月27日 17:04
下一篇 2024年3月27日 17:04

相关推荐

发表评论

登录后才能评论

联系我们

在线咨询:1643011589-QQbutton

手机:13798586780

QQ/微信:1074760229

QQ群:551893940

工作时间:工作日9:00-18:00,节假日休息

关注微信