SpringCloud OpenFeign 服务调用传递 token

业务场景通常微服务对于用户认证信息解析有两种方案

  • 在 gateway 就解析用户的 token 然后路由的时候把 userId 等相关信息添加到 header 中传递下去 。
  • 在 gateway 直接把 token 传递下去,每个子微服务器自己在过滤器解析 token
现在有一个从 A 服务调用 B 服务接口的内部调用业务场景,无论是哪种方案我们都需要把 header 从 A 服务传递到 B 服务 。
RequestInterceptorOpenFeign 给我们提供了一个请求拦截器 RequestInterceptor ,我们可以实现这个接口重写 Apply 方法将当前请求的 header 添加到请求中去,传递给下游服务, RequestContextHolder 可以获得当前线程绑定的 Request 对象
/** Feign 调用的时候传token到下游 */public class FeignRequestInterceptor implements RequestInterceptor {@Overridepublic void apply(RequestTemplate template) {// 从header获取X-tokenRequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();ServletRequestAttributes attr = (ServletRequestAttributes) requestAttributes;HttpServletRequest request = attr.getRequest();String token = request.getHeader("x-auth-token");//网关传过来的 tokenif (StringUtils.hasText(token)) {template.header("X-AUTH-TOKEN", token);}}}复制代码然后在 @FeignClient 中使用
@FeignClient(...configuration = {FeignClientDecoderConfiguration.class, FeignRequestInterceptor.class})public interface AuthCenterClient {复制代码多线程环境下传递 header(一)上面是单线程的情况,假如我们在当前线程中又开启了子线程去进行 Feign 调用,那么是无法从 RequestContextHolder 获取到 header 的,原因很简单,看下 RequestContextHolder 源码就知道了,它里面是一个 ThreadLocal ,线程都变了,那肯定获取不到主线程请求里面的 requestAttribute 了 。
原因已经清楚了,现在想办法去解决它 。观察
RequestContextHolder.getRequestAttributes() 方法源码
public static RequestAttributes getRequestAttributes() {RequestAttributes attributes = requestAttributesHolder.get();if (attributes == null) {attributes = inheritableRequestAttributesHolder.get();}return attributes;}复制代码注意到如果当前线程拿不到 RequestAttributes ,他会从
inheritableRequestAttributesHolder 里面拿着,再仔细观察发现源码设置 RequestAttributes 到 ThreadLocal 的时候有这样一个重载方法
/** * 给当前线程绑定属性 * @param inheritable 是否要将属性暴露给子线程 */public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) {//......}复制代码这特喵的完美符合我们的需求,现在我们的问题就是子线程没有拿到主线程的 RequestContextHolder 里面的属性 。在业务代码中:
RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);log.info("主线程任务....");new Thread(() -> {log.info("子线程任务开始...");UserResponse response = client.getById(3L);}).start();复制代码开发环境测试之后发现子线程已经能够从 RequestContextHolder 拿到主线程的请求对象了 。
分析 inheritableRequestAttributesHolder 原理观察源码我们可以看到这个属性的类型是
NamedInheritableThreadLocal 它继承了 InheritableThreadLocal  。还记得去年我第一次遇到开启多线程跨服务请求的时候始终不能理解为什么这玩意能把当前线程绑定的对象暴露给子线程 。前几天 debug 了一下
InheritableThreadLocal.set() 方法恍然大悟 。
其实这个东西对 Thread、ThreadLocal 有了解就会知道,在 Thread 构造方法里面有这样一段代码
//...Thread parent = currentThread(); //创建子线程的时候先拿父线程//...if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals;//...复制代码其实我们创建子线程的时候会先拿父线程,判断父线程里面的 inheritableThreadLocals 是不是有价值,由于上面
RequestContextHolder.setRequestAttributes(xxx,true) 设置了 true ,所以父线程的 inheritableThreadLocals 是有 requestAttributes 的 。这样创建子线程后,子线程的 inheritableThreadLocals 也有值了 。所以后面我们在子线程中获取 requestAttributes 是能获取到的 。
这样就真的解决问题了吗?从非 web 从表面来看,的确是解决了这个问题,但是在我们的 web 场景中并非如此 。经过反复的测试,我们会发现子线程并不是每次都能获取到 header ,进而我们发现了这与父子线程的结束顺序有关,如果父线程早与子线程结束,那么子线程就获取不到 header ,反之子线程能获取到 header  。


推荐阅读