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
里面拿,再仔细观察发现源码设置 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
。
分析 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
类
在 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


猜你喜欢
- 先看看效果图:package com.fenghuo.struts.download;import java.net.URLEncoder;
- Feign远程调用Multipartfile参数今天在写业务代码的时候遇到的问题, 前端请求A服务,能正确把参数给到A服务<参数里面包
- 安卓自定义分段式的进度条,供大家参考,具体内容如下前一段时间公司新项目接到一个新需求,其中界面需要用到一个分段式的进度条,找了半天没有发现类
- 在使用spring框架中我们都知道,某个类如果使用了@Service、@Autowire 这种依赖注入的方式引用了其他对象,在另外一个类中,
- 前言最近碰到了Mybatis一对多查询的场景,在这里总结对比下常见的两种实现方式。本文以常见的订单表和订单详情表来举例说明;数据库表准备订单
- /** * @param h *
- 在Java中创建一个线程有两种方法:继承Thread类和实现Runnable接口。下面通过两个例子来分析两者的区别:1)继承Thread类p
- 这里写链接内容仿映客送小礼物的特效,顺便复习一下属性动画,话不多说先看效果图。需求分析可以看到整个动画有几部分组成,那我们就把每个部分拆分出
- 目录1、下面的代码运行的结果是:2、下面有关java实例变量,局部变量,类变量和final变量的说法,错误的是?3、执行如下代码段后,变量s
- 本文实例讲述了Android编程实现仿QQ发表说说,上传照片及弹出框效果。分享给大家供大家参考,具体如下:代码很简单,主要就是几个动画而已,
- 最近的需求有一个自动发布的功能, 需要做到每次提交都要动态的添加一个定时任务代码结构1. 配置类package com.orion.ops.
- 在实际开发中经常需要了解具体对象的类型,所以经常会使用GetType()和typeof()、尽管可以得到相应的类型、但两者之间也存在一些差别
- 本来就是基础知识,不能丢的太干净,今天竟然花了那么长的时间才写出来,记一下。有如下的一颗完全二叉树:先序遍历结果应该为:1 2&
- 本文实例讲述了C#编程获取客户端计算机硬件及系统信息功能。分享给大家供大家参考,具体如下:这里使用C#获取客户端计算机硬件及系统信息 ,包括
- 在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体
- 第1部分 HashMap介绍HashMap简介HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。HashMap
- 1、Java字符串在 Java 中字符串被作为 String 类型的对象处理。 String 类位于 java.lang 包中,默认情况下该
- 本文实例为大家分享了Retrofit2 RxJava2实现Android App自动更新,具体内容如下功能解析自动更新可以说已经是App的标
- 根据用户系统时区动态展示时间当我们使用SpringBoot+Mysql开发系统时,总是统一设置UTC+8时区,这样用户在任何地区访问系统,展
- 前言学习自定义view,想找点东西耍一下,刚好看到抖音的点赞效果不错,尝试一下。抖音效果: 话不多说,先上代码:public class L