我们在写服务端项目的时候,总会限制对某些资源的访问,最常见的就是要求用户先登录才能访问资源,当用户登录后就会将此次会话信息保存进session,同时返回给浏览器指定的cookie键值,下次浏览器再次访问,请求头中就会携带这个cookie,我们也以次来识别用户的登录状态,做出正确响应。


1、问题描述

​ 有时候,我们先行登录,然后访问服务A的某个方法,请求头中携带cookie,标识我们已经登录。但若是我们访问的目标方法在执行过程中使用feign进行原程调用服务B(假设不存在跨域),而服务B也要先判断登录状态,我们可能发现服务B会调用失败,或者说拿不到数据,理由是服务B认为我们并未登录。而这时,如果我们直接从浏览器访问服务B的这个方法却能得到一个成功的响应。.png

​ 结合上图所示,浏览器在调用order服务时,请求头里有Cookie,但在order服务远程调用cart服务时,请求头不再携带之前的Cookie。这是因为Feign在远程调用的时候会创建一个新的request请求 ,因此远程调用便不会再携带之前的Cookie。

2、解决方案

​ 解决的方法其实很简单,无非就是将之前请求头的Cookie拿到,然后再放入新的请求头当中即可。创建一个拦截器,在远程调用的时候执行更新新的request请求头的操作。feign在创建新的request对象时,会调用一系列容器中的RequestInterceptor对象,执行其apply方法,对这个创建好的request进行增强,再去真正执行请求。但是默认情况下容器中不存在这类拦截器对象。
​ 我们可以自己向容器中注册一个RequestInterceptor,在其apply方法体内,获取到原始request,将其数据取出,赋值到新的request中,完成请求头的同步。
RequestContextHolder借助ThreadLocal将每一个原始请求与tomcat为其分配的线程绑定,之后,只要在同个线程内,随时随地都可轻易获取到原始request
​ 而我们是在apply方法体内,通过 RequestContextHolder.getRequestAttributes() 获取的。RequestContextHolder是借助ThreadLocal将每一个原始请求与tomcat为其分配的线程绑定,之后,只要在同个线程内,随时随地都可轻易获取到原始request。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
public class GuliFeignConfig {
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor(){
@Override
public void apply(RequestTemplate requestTemplate) {
//1、RequestContextHolder拿到刚进来的请求
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
if(request != null){
//同步请求数据
String cookie = request.getHeader("Cookie");
//给新请求同步了老请求的Cookie
requestTemplate.header("Cookie",cookie);
}

}
};
}
}

​ 但是如果你的feign调用出现在异步线程体内,RequestInterceptor拦截到你时,你再使用RequestContextHolder,获取的已经不是原来线程,必然无法获取到原请求,只能拿到与当下线程绑定的request,甚至直接得到一个null,引发空指针异常。

异步编排解决

​ 这要如何解决?很简单,提前同步数据。进入新线程之前,拿出原线程绑定的requestAttributes,在新的线程体内,feign调用之前,将其赋值到本线程绑定的request中,这样,在执行feign方法,被拦截器拦截时,当前线程绑定的request已不为空。(RequestContextHolder写在哪就绑定的是哪个线程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//获取之前的请求
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
//每一个线程都来共享之前的请求
RequestContextHolder.setRequestAttributes(requestAttributes);
//1、远程查询所有的收货地址列表
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
confirmVo.setAddress(address);
}, threadPoolExecutor);

CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {

//每一个线程都来共享之前的请求
RequestContextHolder.setRequestAttributes(requestAttributes);
//2、远程查询购物车所有选中的购物项
List<OrderItemVo> currentUserCartItem = cartFeignService.getCurrentUserCartItem();
confirmVo.setItems(currentUserCartItem);
}, threadPoolExecutor);