SpringCloud OpenFeign 服务调用传递 token的场景分析

作者:暮色妖娆丶 时间:2022-12-26 22:24:07 

业务场景

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

  • gateway 就解析用户的 token 然后路由的时候把 userId 等相关信息添加到 header 中传递下去。

  • gateway 直接把 token 传递下去,每个子微服务自己在过滤器解析 token

现在有一个从 A 服务调用 B 服务接口的内部调用业务场景,无论是哪种方案我们都需要把 header 从 A 服务传递到 B 服务。

RequestInterceptor

OpenFeign 给我们提供了一个请求 * RequestInterceptor ,我们可以实现这个接口重写 apply 方法将当前请求的 header 添加到请求中去,传递给下游服务,RequestContextHolder 可以获得当前线程绑定的 Request 对象

/** Feign 调用的时候传token到下游 */
public class FeignRequestInterceptor implements RequestInterceptor {
 @Override
 public void apply(RequestTemplate template) {
   // 从header获取X-token
   RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
   ServletRequestAttributes attr = (ServletRequestAttributes) requestAttributes;
   HttpServletRequest request = attr.getRequest();
   String token = request.getHeader("x-auth-token");//网关传过来的 token
   if (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 里面拿,再仔细观察发现源码设置 RequestAttributesThreadLocal 的时候有这样一个重载方法

/**
* 给当前线程绑定属性
* @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

分析 inheritableRequestAttributesHolder 失效原因

其实标题并不严谨,因为子线程获取不到请求的 header 并不是因为 inheritableRequestAttributesHolder 失效。这个原因当初我也很奇怪,于是我从网上看到一篇文章,它是这么写的。

在源码中ThreadLocal对象保存的是RequestAttributes attributes;这个是保存的对象的引用一旦父线程销毁了,那RequestAttributes也会被销毁,那RequestAttributes的引用地址的值就为null**;**虽然子线程也有RequestAttributes的引用,但是引用的值为null了。

真的是这样吗??我怎么看怎么感觉不对......于是我自己验证了下

@GetMapping("/test")
public void test(HttpServletRequest request) {
   RequestAttributes attr = RequestContextHolder.getRequestAttributes();
   log.info("父线程:RequestAttributes:{}", attr);
   RequestContextHolder.setRequestAttributes(attr, true);
   log.info("父线程:SpringMVC:request:{}",request);
   log.info("父线程:x-auth-token:{}",request.getHeader("x-auth-token"));
   ServletRequestAttributes attr1 = (ServletRequestAttributes) attr;
   HttpServletRequest request1 = attr1.getRequest();
   log.info("父线程:request:{}",request1);
   new Thread(
           () -> {
               try {
                   TimeUnit.SECONDS.sleep(3);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               RequestAttributes childAttr = RequestContextHolder.getRequestAttributes();
               log.info("子线程:RequestAttributes:{}",childAttr);
               ServletRequestAttributes childServletRequestAttr = (ServletRequestAttributes) childAttr;
               HttpServletRequest childRequest = childServletRequestAttr.getRequest();
               log.info("子线程:childRequest:{}",childRequest);
               String childToken = childRequest.getHeader("x-auth-token");
               log.info("子线程:x-auth-token:{}",childToken);
           }).start();
}

观察日志

父线程:RequestAttributes:org.apache.catalina.connector.RequestFacade@ea25271
父线程:SpringMVC:request:org.apache.catalina.connector.RequestFacade@ea25271
父线程:x-auth-token:null
父线程:request:org.apache.catalina.connector.RequestFacade@ea25271

子线程:RequestAttributes:org.apache.catalina.connector.RequestFacade@ea25271
子线程:childRequest:org.apache.catalina.connector.RequestFacade@ea25271
子线程:x-auth-token:{}:null

很明显子线程拿到了 RequestAttitutes 对象,而且和父线程是同一个,这就推翻了上面的说法,并不是引用变为 null 了导致的。那么到底是什么原因导致父线程结束后,子线程就拿不到 request 对象里面的 header 属性了呢?

我们可以猜测一下,既然父线程和子线程拿到的 request 对象是同一个,并且在子线程代码中 request 对象还不是 null,但是属性没了,那应该是请求结束之后某个地方对 request 对象进行了属性移除。我们跟随 RequestFacade 类去寻找真理,寻找寻找再寻找......终于我发现了真相在 org.apache.coyote.Request

SpringCloud OpenFeign 服务调用传递 token的场景分析

Tomcat 内部,请求结束后会对 request 对象重置,把 header 等属性移除,是因为这样如果父线程提前结束,我们在子线程中才无法获取 request 对象的 header

或许你可以再思考一下 Tomcat 为什么要这么做?

多线程环境下传递 header(二)

既然 RequestContextHolder.setRequestAttributes(attr, true); 也不能完全实现子线程能够获取父线程的 header ,那么我们如何解决呢?

控制主线程在子线程结束后再结束

这是最简单的方法,我把父线程挂起来,等子线程任务都执行完了,再结束父线程,这样就不会出现子线程获取不到 header 的情况了。最简单的,我们可以用 ExecutorCompletionService 实现。

重新保存 request 的 header

上面我们已经知道了获取不到 header 是因为 request 对象的 header 属性被移除了,那么我们只需要自己定义一个数据结构 ThreadLocal 重新在内存中保存一份 header 属性即可。我们可以定义一个请求 * ,在 * 中获取 headers 放到自定义的结构中。

定义结构

public class RequestHeaderHolder {
   private static final ThreadLocal<Map<String,String>> REQUEST_HEADER_HOLDER = new InheritableThreadLocal<>(){
       @Override
       protected Map<String, String> initialValue() {
           return new HashMap<>();
       }
   };
   //...省略部分方法
}

*

public class RequestHeaderInterceptor implements HandlerInterceptor {
   @Override
   public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
       Enumeration<String> headerNames = request.getHeaderNames();

while (headerNames.hasMoreElements()){
           String s = headerNames.nextElement();
           RequestHeaderHolder.set(s,request.getHeader(s));
       }
       return true;
   }

@Override
   public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
       RequestHeaderHolder.remove(); //注意一定要remove
   }
}

然后将这个 * 添加到 InterceptorRegistry 即可。这样我们在子线程中就可以通过 RequestHeaderHolder 获取请求到 header

来源:https://juejin.cn/post/7123096319371001870

标签:SpringCloud,OpenFeign,传递,服务调用
0
投稿

猜你喜欢

  • SpringBoot实现多个子域共享cookie的示例

    2021-08-02 17:56:47
  • Windows编写jar启动脚本和关闭脚本的操作方法

    2021-05-28 04:36:58
  • Java设计模式之观察者模式_动力节点Java学院整理

    2022-01-14 12:27:47
  • 为SpringBoot服务添加HTTPS证书的方法

    2023-10-11 03:03:22
  • Android Data Binding 在 library module 中遇到错误及解决办法

    2021-08-31 12:16:31
  • idea在用Mybatis时xml文件sql不提示解决办法(提示后背景颜色去除)

    2023-11-09 01:45:51
  • 使用java的Calendar对象获得当前日期

    2023-10-11 16:24:23
  • selenium+java破解极验滑动验证码的示例代码

    2022-11-19 21:52:01
  • C#设计模式实现之迭代器模式

    2023-05-31 19:21:58
  • java返回集合为null还是空集合及空集合的三种写法小结

    2021-08-18 05:37:48
  • 基于java构造方法Vector修改元素源码分析

    2023-11-25 10:54:56
  • java线程之用Thread类创建线程的方法

    2023-02-09 18:35:07
  • Java异常简介和架构_动力节点Java学院整理

    2022-09-03 07:07:52
  • C# 调用命令行执行Cmd命令的操作

    2022-08-03 10:43:26
  • Java 并发编程之ThreadLocal详解及实例

    2023-09-05 13:48:02
  • kill命令在Java应用中使用的注意事项小结

    2023-11-11 13:01:55
  • Android学习教程之悬浮窗菜单制作(9)

    2022-03-27 08:17:13
  • Android UI动态设置带有Stroke渐变色背景Drawable

    2023-12-02 15:44:56
  • Mybatis拦截器的实现介绍

    2023-07-04 04:23:31
  • Java 两种延时thread和timer详解及实例代码

    2022-08-30 03:42:14
  • asp之家 软件编程 m.aspxhome.com