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
猜你喜欢
- 问题在使用 Abp 框架的后台作业时,当后台作业抛出异常,会导致整个程序崩溃。在 Abp 框架的底层执行后台作业的时候,有 try/catc
- 示例1:public static String hello() { String s = "商务&qu
- 1.注解方式,yml文件配置上以下就可以直接使用mybatis-plus: mapper-locations: classpath:mapp
- 主要从以下十几个方面对Hibernate做总结,包括Hibernate的检索方式,Hibernate中对象的状态,Hibernate的3种检
- springboot集成swagger3swagger3的springboot启动器jar包<!-- https://mvnrepos
- SDK是Software Development Kit的缩写,中文意思是“软件开发工具包”。这是一个覆盖面相当广泛的名词,可以这么说:辅助
- Android 消息机制1.概述Android应用启动时,会默认有一个主线程(UI线程),在这个线程中会关联一个消息队列(MessageQu
- intellij idea是一款非常优秀的软件开发工具,它拥有这强大的插件体系,可以帮助开发者完成很多重量级的功能。今天,我们来学习一下如何
- 前言作为大数据家族中的重要一员,在大数据以及海量数据存储方面,hbase具有重要的地方,本篇将从java对hbase的操作上,进行详细的说明
- 一、@RequestMapping@RequestMapping注解的源码:@Target({ElementType.TYPE, Eleme
- 本文实例讲述了Java Swing实现简单的体重指数(BMI)计算器功能。分享给大家供大家参考,具体如下:BMI,Body Mass Ind
- 1、static关键字1.1 使用static关键字定义属性在讲解static定义属性操作之前,首先编写如下一道程序。现在定义一个表示中国人
- 1、注解@PathVariable:将请求url中的占位符参数与控制器方法入参绑定起来(Rest风格请求)@RequestHeader:获取
- 本地仓库主要是一种缓存,当你使用远程仓库中下载组件后,它下一次会优先从本地进行加载,一般位于USER_HOME/.m2目录下,我们自己也可以
- @RequestMapping注解注意点类上加没加@RequestMappin注解区别1.如果类上加了 @RequestMappin注解,那
- 本文主要对SpringBoot2.x参数校验进行简单总结,其中SpringBoot使用的2.4.5版本。一、引入依赖<dependen
- 本文实例讲述了java版微信公众平台消息接口应用方法。分享给大家供大家参考,具体如下:微信公众平台现在推出自动回复消息接口,但是由于是接口内
- 一、什么是CharacterEncodingFilter官方解释如下是spring内置过滤器的一种,用来指定请求或者响应的编码格式。在web
- 一.背景本文主要介绍Java 8中时间的操作方法java.util.Date是用于表示一个日期和时间的对象(注意与java.sql.Date
- 路径分隔符:Windows下是“\”unix|linux下是“/”考虑到程序的可移植性,创建文件时建议大家选用"/",因