您当前的位置:首页 > 电脑百科 > 程序开发 > 框架

SpringCloud OpenFeign 服务调用传递 token

时间:2022-07-24 11:57:14  来源:  作者:互联网架构小马哥

业务场景

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

  • 在 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 类

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



Tags:SpringCloud   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
如何实现SpringCloud全链路灰色发布?
灰度发布(Gray Release,也称为灰度发布或金丝雀发布)是指在软件或服务发布过程中,将新版本的功能或服务以较小的比例引入到生产环境中,仅向部分用户或节点提供新功能的一种发布策...【详细内容】
2023-11-14  Search: SpringCloud  点击:(224)  评论:(0)  加入收藏
SpringCloud OpenFeign整合Ribbon实现负载均衡及源码分析
负载均衡器在分布式网络中扮演着非常重要的角色。通过负载均衡,可以实现更好的性能和可靠性,同时提高系统的可扩展性和弹性。目前,SpringCloud体系中,主要使用的有两种:Netflix的...【详细内容】
2023-11-09  Search: SpringCloud  点击:(242)  评论:(0)  加入收藏
「SpringCloud」微信小程序授权登录流程设计和实现
&emsp;在前面的设计和实现中,我们的微服务开发平台通过JustAuth来实现第三方授权登录,通过集成公共组件,着实减少了很多工作量,大多数的第三方登录直接通过配置就可以实现。而在...【详细内容】
2023-03-27  Search: SpringCloud  点击:(246)  评论:(0)  加入收藏
在家玩不起微服务springcloud,后台崩掉了
本地装聊mysql数据库,真吃内存啊,微服务一起来,idea 开发工具就疯狂吃内存,前端一启动,node.js 就狂吃,网页再吃点,16g内存一会儿就崩了,如果再玩一下微信开发工具,估计都一点跑不起...【详细内容】
2022-10-23  Search: SpringCloud  点击:(264)  评论:(0)  加入收藏
SpringCloud中gateWay启动报错
一,报错内容当使用gateway的时候,我们可能会在gateway项目中引入一些额外的包,比如webmvc,starter-web等,当然默认是不需要这些的,而且这些放到里面也是会报错的,一般使用的场景,比...【详细内容】
2022-09-15  Search: SpringCloud  点击:(1050)  评论:(0)  加入收藏
万字长文带你吃透SpringCloudGateway工作原理+动态路由+源码解析
Spring Cloud GatewaySpring Cloud 2.x 实 现 了 社 区 生 态 下 的 Spring CloudGateway(简称SCG)微服务网关项目。Spring Cloud Gateway基于WebFlux框架开发,目标是替换掉Zuu...【详细内容】
2022-09-13  Search: SpringCloud  点击:(394)  评论:(0)  加入收藏
SpringCloud集成Resilience4j实现熔断器
前言 在文章《小谈Springcloud中的几个主流熔断器》,我们介绍了SpingCloud架构中的几个主流熔断器,其中SpringCloud官方推荐的Resilience4j作为2020.x以后的新秀,远远没有hystr...【详细内容】
2022-09-07  Search: SpringCloud  点击:(361)  评论:(0)  加入收藏
Spring、SpringBoot和SpringCloud的基础入门
在Java项目开发过程中,我们常常会使用开源的基础框架再引入一些中间件来快速的搭建一个项目,然后进行二次开发。先说中间件,有我们比较常用的RabbitMQ/RocketMQ/Kafka、Redis、...【详细内容】
2022-09-04  Search: SpringCloud  点击:(353)  评论:(0)  加入收藏
SpringCloud gateway自定义请求的 httpClient
SpringCloud gateway 在实现服务路由并请求的具体过程是在 org.springframework.cloud.gateway.filter.NettyRoutingFilter 的过滤器中,该过滤器封装了具体的请求参数,以及根...【详细内容】
2022-07-30  Search: SpringCloud  点击:(548)  评论:(0)  加入收藏
SpringCloud OpenFeign 服务调用传递 token
业务场景通常微服务对于用户认证信息解析有两种方案 在 gateway 就解析用户的 token 然后路由的时候把 userId 等相关信息添加到 header 中传递下去。 在 gateway 直接把 to...【详细内容】
2022-07-24  Search: SpringCloud  点击:(331)  评论:(0)  加入收藏
▌简易百科推荐
Qt与Flutter:在跨平台UI框架中哪个更受欢迎?
在跨平台UI框架领域,Qt和Flutter是两个备受瞩目的选择。它们各自具有独特的优势,也各自有着广泛的应用场景。本文将对Qt和Flutter进行详细的比较,以探讨在跨平台UI框架中哪个更...【详细内容】
2024-04-12  刘长伟    Tags:UI框架   点击:(7)  评论:(0)  加入收藏
Web Components实践:如何搭建一个框架无关的AI组件库
一、让人又爱又恨的Web ComponentsWeb Components是一种用于构建可重用的Web元素的技术。它允许开发者创建自定义的HTML元素,这些元素可以在不同的Web应用程序中重复使用,并且...【详细内容】
2024-04-03  京东云开发者    Tags:Web Components   点击:(11)  评论:(0)  加入收藏
Kubernetes 集群 CPU 使用率只有 13% :这下大家该知道如何省钱了
作者 | THE STACK译者 | 刘雅梦策划 | Tina根据 CAST AI 对 4000 个 Kubernetes 集群的分析,Kubernetes 集群通常只使用 13% 的 CPU 和平均 20% 的内存,这表明存在严重的过度...【详细内容】
2024-03-08  InfoQ    Tags:Kubernetes   点击:(23)  评论:(0)  加入收藏
Spring Security:保障应用安全的利器
SpringSecurity作为一个功能强大的安全框架,为Java应用程序提供了全面的安全保障,包括认证、授权、防护和集成等方面。本文将介绍SpringSecurity在这些方面的特性和优势,以及它...【详细内容】
2024-02-27  风舞凋零叶    Tags:Spring Security   点击:(62)  评论:(0)  加入收藏
五大跨平台桌面应用开发框架:Electron、Tauri、Flutter等
一、什么是跨平台桌面应用开发框架跨平台桌面应用开发框架是一种工具或框架,它允许开发者使用一种统一的代码库或语言来创建能够在多个操作系统上运行的桌面应用程序。传统上...【详细内容】
2024-02-26  贝格前端工场    Tags:框架   点击:(52)  评论:(0)  加入收藏
Spring Security权限控制框架使用指南
在常用的后台管理系统中,通常都会有访问权限控制的需求,用于限制不同人员对于接口的访问能力,如果用户不具备指定的权限,则不能访问某些接口。本文将用 waynboot-mall 项目举例...【详细内容】
2024-02-19  程序员wayn  微信公众号  Tags:Spring   点击:(43)  评论:(0)  加入收藏
开发者的Kubernetes懒人指南
你可以将本文作为开发者快速了解 Kubernetes 的指南。从基础知识到更高级的主题,如 Helm Chart,以及所有这些如何影响你作为开发者。译自Kubernetes for Lazy Developers。作...【详细内容】
2024-02-01  云云众生s  微信公众号  Tags:Kubernetes   点击:(58)  评论:(0)  加入收藏
链世界:一种简单而有效的人类行为Agent模型强化学习框架
强化学习是一种机器学习的方法,它通过让智能体(Agent)与环境交互,从而学习如何选择最优的行动来最大化累积的奖励。强化学习在许多领域都有广泛的应用,例如游戏、机器人、自动驾...【详细内容】
2024-01-30  大噬元兽  微信公众号  Tags:框架   点击:(72)  评论:(0)  加入收藏
Spring实现Kafka重试Topic,真的太香了
概述Kafka的强大功能之一是每个分区都有一个Consumer的偏移值。该偏移值是消费者将读取的下一条消息的值。可以自动或手动增加该值。如果我们由于错误而无法处理消息并想重...【详细内容】
2024-01-26  HELLO程序员  微信公众号  Tags:Spring   点击:(95)  评论:(0)  加入收藏
SpringBoot如何实现缓存预热?
缓存预热是指在 Spring Boot 项目启动时,预先将数据加载到缓存系统(如 Redis)中的一种机制。那么问题来了,在 Spring Boot 项目启动之后,在什么时候?在哪里可以将数据加载到缓存系...【详细内容】
2024-01-19   Java中文社群  微信公众号  Tags:SpringBoot   点击:(91)  评论:(0)  加入收藏
站内最新
站内热门
站内头条