Feign远程调用丢失请求头问题

2023-10-27

在业务中,需要使用A、B两个模块,这些模块使用了SpringSession共享Session数据。在B模块中的业务需要用户登录后才能操作。当A调用B的业务时,在B模块中获取不到用户的Session信息,导致B模块判定该请求用户没有登录导致A模块拿不到所需数据。问题是A模块可以拿到该用户的登录信息并且,已经使用了SpringSession进行共享Session数据。

找出问题原因

使用Feign发送远程调用

发送远程请求

当请求进到B服务时被其登录验证拦截器拦截,试图去Session中拿登录信息时,结果为 null(已确定登录)

B服务检查登录拦截器

我们都知道,session的原理是需要通过 cookie 中的某个值(jesessionid)来确定一个Session对象,在B模块中拿不到用户数据是因为无法通过指定cookie来获取这个到Session对象。

为了解决这个问题,需要Debug一下Feign的流程。

Feign 流程

查询发送请求,来到远程调用代码打断点,setup into 进去检查

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MAfI4PB9-1645798237261)(/Users/clover/Library/Application Support/typora-user-images/截屏2022-02-25 下午8.27.51.png)]

在判断不是equals、hashCode、toString等方法时,执行 invoke方法进行远程调用,setup into进入

进入invoke方法

invoke方法中,首先去创建一个的请求模板,这个模板包含了我们的请求头等请求信息

创建request请求模板

并没有其它特殊处理就直接调用 Client 发送请求了
请求发送

从 feign 的流程看出,它是直接给我门创建一个新的请求,并没有给我们封装浏览器给A服务发送请求时携带的请求头等信息。

解决

在debug过程中,发现 executeAndDecode 方法中在调用 Client 发送请求时有一个 this.targetRequest(template); 操作,它返回一个 RequestClient 发送请求时代的就是这个request对象。

Request

targetRequest 方法中发现,他会拿到一个什么拦截器 requestInterceptors,然后便利调用它的 apply 方法并把它创建的请求模板传过去 RequestTemplate ,而这个 RequestInterceptors 是在容器中拿的,所以我们只需要在容器中添加一个 RequestInterceptors 组件即可。

targetRequest内部

例如:

@Component
public class FeignFillContent implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        // 同步cookie
        requestTemplate.header("Cookie", "xxx");
    }
}

那么问题来了,我们应该如何拿到Cookie?其实这个问题也很简单,我们可以搞一个拦截器,然后把HttpServletRequest保存在 ThreadLocal 中即可

@Component
public class LoginInterceptor implements HandlerInterceptor {

  public final static ThreadLocal<HttpServletRequest> THREAD_LOCAL_REQUEST = new ThreadLocal<>();

  @Override
  public boolean preHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler) throws Exception {
    THREAD_LOCAL_REQUEST.set(request);
    ...
      return HandlerInterceptor.super.preHandle(request, response, handler);
  }

  @Override
  public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    ...
      THREAD_LOCAL_REQUEST.remove();
    HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
  }
}

SpringBoot 也有这种工具提供,不需要我们额外写多余的类 RequestContextHolder,这个类的原理也是使用 ThreadLocal

@Component
public class FeignFillContent implements RequestInterceptor {
  @Override
  public void apply(RequestTemplate requestTemplate) {
    // 获取请求上下文
    ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = requestAttributes.getRequest();

    // 同步cookie
    String myCookies = request.getHeader("Cookie");
    requestTemplate.header("Cookie", myCookies);
  }
}

测试一下,B服务成功拿到用户登录信息

测试

异步环境下问题重现

在单线程环境下没毛病,但在多线程,例如 CompletableFuture 下还是会出现问题,这次问题是我请求拦截器抛出空指针异常。

这个问题是因为 ThreadLocal 底层时Map,键使用的是当前线程对象,所以在单线程环境下没问题,一使用异步就出现问题。因为异步后是一个新的线程,已经不再是我们原来处理请求的那个线程了,所以通过当前线程对象是拿不到ThreadLocal中的数据的。

// 获取购物项
CompletableFuture<Void> cartItemFuture = CompletableFuture.runAsync(() -> {
  R cartItem = cartFeignService.getCurrentUserCartItem();
  ...
}, executor);

解决

这个问题也很简单,就是共享 ThreadLocal也就是将指定 ThreadLocal 复制到指定线程的 ThreadLocal

@Override
public OrderConfirmVo confirmOrder() {
  ...
    RequestAttributes myReqContext = RequestContextHolder.currentRequestAttributes();
  // 查询会员所有收货地址
  CompletableFuture<Void> memberFuture = CompletableFuture.runAsync(() -> {
    // 复制一份ThreadLocal
    RequestContextHolder.setRequestAttributes(myReqContext);
    R memberReceiveAddress = memberFeignService.getMemberReceiveAddress(mrv.getId());
    ...
  }, executor);

  // 获取购物项
  CompletableFuture<Void> cartItemFuture = CompletableFuture.runAsync(() -> {
    // 复制一份ThreadLocal
    RequestContextHolder.setRequestAttributes(myReqContext);
    R cartItem = cartFeignService.getCurrentUserCartItem();
    ...
  }, executor);

  CompletableFuture.allOf(memberFuture, cartItemFuture).join();
  return vo;
}

结果

欢迎访问我的博客:https://www.ctong.top
博文地址:https://www.ctong.top/2022/02/25/Feign%E8%BF%9C%E7%A8%8B%E8%B0%83%E7%94%A8%E4%B8%A2%E5%A4%B1%E8%AF%B7%E6%B1%82%E5%A4%B4%E9%97%AE%E9%A2%98/

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Feign远程调用丢失请求头问题 的相关文章

随机推荐