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

目录
  • 业务场景
  • RequestInterceptor
  • 多线程环境下传递 header(一)
  • 分析 inheritableRequestAttributesHolder 原理
  • 分析 inheritableRequestAttributesHolder 失效原因
  • 多线程环境下传递 header(二)
    • 控制主线程在子线程结束后再结束
    • 重新保存 request 的 header
  • 结语

业务场景

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

  • 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

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

结语

本篇文章简单介绍 OpenFeign 调用传递 header ,以及多线程环境下可能会出现的问题。其中涉及到 ThreadLocal 的相关知识,如果有同学对 ThreadLocal、InheritableThreadLocal 不清楚的可以留言,后面出一篇 ThreadLocal 的文章。

到此这篇关于SpringCloud OpenFeign 服务调用传递 token的场景分析的文章就介绍到这了,更多相关SpringCloud OpenFeign传递 token内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • SpringCloud升级2020.0.x版之OpenFeign简介与使用实现思路

    目录 OpenFeign 的由来和实现思路 OpenFeign 简介 OpenFeign 基本使用 本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent OpenFeign 的由来和实现思路 在微服务系统中,我们经常会进行 RPC 调用.在 Spring Cloud 体系中,RPC 调用一般就是 HTTP 协议的调用.对于每次调用,基本都要经过如下步骤: 找到微服务实例列表并选择一个实例 调用参数序列化 使用 Http 客户端将请求发送出去

  • 详解SpringCloud-OpenFeign组件的使用

    思考: 使用RestTemplate+ribbon已经可以完成服务间的调用,为什么还要使用feign? String restTemplateForObject = restTemplate.getForObject("http://服务名/url?参数" + name, String.class); 存在问题: 1.每次调用服务都需要写这些代码,存在大量的代码冗余 2.服务地址如果修改,维护成本增高 3.使用时不够灵活 说明 https://cloud.spring.io/sprin

  • Springcloud基于OpenFeign实现服务调用代码实例

    1.依赖 <!--引入open feign依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies> 2.启动注解 @SpringBootApplication @Enabl

  • SpringCloud学习笔记之OpenFeign进行服务调用

    目录 前言 1.OpenFeign 1.1.OpenFeign概述 1.2.OpenFeign的使用步骤 1.3.超时控制 1.3.1.是什么? 1.3.2.修改代码设置超时错误 1.3.3.进行超时配置 1.4.日志打印 1.4.1.是什么? 1.4.2.日志级别 1.4.3.如何开启日志打印 总结 前言 Feign是一个声明式的Web服务客户端,是面向接口编程的.也就是说使用Feign,只需要创建一个接口并使用注解方式配置它,就可以完成对微服务提供方的接口绑定. 在使用RestTemplat

  • SpringCloud 服务负载均衡和调用 Ribbon、OpenFeign的方法

    1.Ribbon Spring Cloud Ribbon是基于Netflix Ribbon实现的-套客户端―负载均衡的工具. 简单的说,Ribbon是Netlix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用.Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等.简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器.我们很容易使用Ribbon实现自定义的负载均

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

    目录 业务场景 RequestInterceptor 多线程环境下传递 header(一) 分析 inheritableRequestAttributesHolder 原理 分析 inheritableRequestAttributesHolder 失效原因 多线程环境下传递 header(二) 控制主线程在子线程结束后再结束 重新保存 request 的 header 结语 业务场景 通常微服务对于用户认证信息解析有两种方案 在 gateway 就解析用户的 token 然后路由的时候把 us

  • Feign如何解决服务之间调用传递token

    目录 解决服务之间调用传递token Feign有提供一个接口RequestInterceptor 调用方式 Feign调用服务各种坑处理 编写被调用服务 编写调用api 编写客户端服务 解决服务之间调用传递token 现在的微服务基本就是SpringSecurity+Oauth2做的授权和认证,假如多个服务直接要通过Fegin来调用,会报错401 a.有做权限处理的服务接口直接调用会造成调用时出现http 401未授权的错误,继而导致最终服务的http 500内部服务器错误 b.解决方式:最方

  • 详解OpenFeign服务调用(微服务)

    目录 OpenFeign服务调用 OpenFeign是什么 Feign能干什么 Feign集成了Ribbon Feign和OpenFeign两者区别 OpenFeign服务调用 OpenFeign超时控制 OpenFeign日志增强 OpenFeign服务调用 OpenFeign是什么 官方文档 Github地址 Feign is a declarative web service client. It makes writing web service clients easier. To u

  • SpringCloud微服务续约实现源码分析详解

    目录 一.前言 二.客户端续约 1.入口 构造初始化 initScheduledTasks()调度执行心跳任务 2.TimedSupervisorTask组件 构造初始化 TimedSupervisorTask#run()任务逻辑 3.心跳任务 HeartbeatThread私有内部类 发送心跳 4.发送心跳到注册中心 构建请求数据发送心跳 三.服务端处理客户端续约 1.InstanceRegistry#renew()逻辑 2.PeerAwareInstanceRegistryImpl#rene

  • SpringCloud Feign 服务调用的实现

    前言 前面我们已经实现了服务的注册与发现(请戳:SpringCloud系列--Eureka 服务注册与发现),并且在注册中心注册了一个服务myspringboot,本文记录多个服务之间使用Feign调用. Feign是一个声明性web服务客户端.它使编写web服务客户机变得更容易,本质上就是一个http,内部进行了封装而已. GitHub地址:https://github.com/OpenFeign/feign 官方文档:https://cloud.spring.io/spring-cloud-

  • SpringCloud Feign服务调用请求方式总结

    前言 最近做微服务架构的项目,在用feign来进行服务间的调用.在互调的过程中,难免出现问题,根据错误总结了一下,主要是请求方式的错误和接参数的错误造成的.在此进行一下总结记录.以下通过分为三种情况说明,无参数,单参数,多参数.每种情况再分get和post两种请求方式进行说明.这样的话,6种情况涵盖了feign调用的所有情况. 有个建议就是为了保证不必要的麻烦,在写feign接口的时候,与我们的映射方法保持绝对一致,同时请求方式,请求参数注解也都不偷懒的写上.如果遵循这种规范,可以避开90%的调

  • Springcloud RestTemplate服务调用代码实例

    1.服务productservices @RestController public class ProductController { @RequestMapping("/product/findAll") public Map findAll(){ Map map = new HashMap(); map.put("111","苹果手机"); map.put("222","苹果笔记本"); return

  • 聊聊SpringCloud中的Ribbon进行服务调用的问题

    目录 1.Robbon 1.1.Ribbon概述 1.2.Ribbon负载均衡演示 1.3.Ribbon核心组件IRule 1.4.Ribbon负载均衡算法 1.4.1.轮询算法原理 负载均衡算法: 1.4.2.RoundRobinRule 源码 1.4.3.手写轮询算法 前置内容(1).微服务理论入门和手把手带你进行微服务环境搭建及支付.订单业务编写(2).SpringCloud之Eureka服务注册与发现(3).SpringCloud之Zookeeper进行服务注册与发现(4).Spring

  • SpringCloud Ribbon与OpenFeign详解如何实现服务调用

    目录 Ribbon 初识Ribbon Ribbon是什么 Ribbon能干什么 使用Ribbon实现负载均衡 RestTemplate三步走 负载均衡算法 轮询算法 OpenFeign 初识OpenFeign 什么是OpenFeign 如何使用OpenFeign OpenFeign超时控制 OpenFeign日志打印 Ribbon 初识Ribbon Ribbon是什么   Ribbon是Netflix发布的开源项目,主要功能是提供对客户端进行负载均衡算法的一套工具,将Netflix的中间层服务连

随机推荐