我们在写服务端项目的时候,总会限制对某些资源的访问,最常见的就是要求用户先登录才能访问资源,当用户登录后就会将此次会话信息保存进session,同时返回给浏览器指定的cookie键值,下次浏览器再次访问,请求头中就会携带这个cookie,我们也以次来识别用户的登录状态,做出正确响应。
有时候,我们先行登录,然后访问服务A的某个方法,请求头中携带cookie,标识我们已经登录。但若是我们访问的目标方法在执行过程中使用feign进行原程调用服务B(假设不存在跨域),而服务B也要先判断登录状态,我们可能发现服务B会调用失败,或者说拿不到数据,理由是服务B认为我们并未登录。而这时,如果我们直接从浏览器访问服务B的这个方法却能得到一个成功的响应。
也就是说:
浏览器--->服务A成功; 服务A-->服务B失败; 浏览器-->服务B失败
结合上面所说,服务AB都会先判断用户登录状态,浏览器直接访问AB时都会带上登录成功后保存的cookie,而服务A通过Feign远程调用B,却被认为未登录,显然,这部分请求头数据丢失。
我们来看下feign远程调用是如何执行的,我们在feign远程调用之处打上断点
invoke
方法,方法内,首先根据请求参数创建一个RequestTemplate
,核心部分是 while(true) 里面的 executeAndDecode()
,while 其实是加了一层重试机制,这里不多说。executeAndDecode
方法,我们看到 targetRequest()
构造出了一个request对象,而最终的response就是这个request请求的执行结果。executeAndDecode
方法,在这个方法体内,会通过 targetRequest
方法创建出一个新的 request 对象,这个新的request会按照我们指定的参数和路径去发送请求,并获得响应结果。问题在于feign自己创建出resttemplate
,再用它构建一个新的request对象去发送请求,而这个新的request不包含任何请求头信息。我们应该在它创造出这个request之后,在它真正发送请求之前,把原始请求头中的数据给它复制过去。
我们来看一下feign最后构建出创建request对象的 targetRequest
方法
我们发现这里面会有调用了一系列 RequestInterceptor
的apply
方法对其进行增强,最后才返回,只不过默认情况下这些拦截器是空的。
因此 ,我们需要需要自己实现一个 RequestInterceptor
,在它的apply方法中将原始请求头中的数据同步到feign创建出的新的request中,并且将这个拦截器注入容器中,这样feign在执行目标方法之前会被其拦截,对其先进行增强。
@Component
public class FeignBeforeExecInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
// 拿到原始请求头数据
String cookie = request.getHeader("Cookie");
if (!StringUtils.isEmpty(cookie)) {
// 同步
template.header("Cookie", cookie);
}
}
}
比较难处理的地方在于,我们如何拿到原始的request对象,spring提供了一个叫 RequestContextHolder
对象帮我们解决这个难题,通过它的 getRequestAttributes
方法或者 currentRequestAttributes
方法就能获取到原始请求数据。关于这两个方法的区别,可简单认为,前者如果获取失败,会返回null;而后者会抛出异常。
还有个问题是这个 RequestContextHolder
是如何保存原始请求的,以至于我们在任何时候都能很方便的拿到,而不是像只能在controller层通过方法参数获取。其实如果你细心看上面的源码图片中的注释的话,就能看到它写的是获取与当前线程绑定的请求数据
我们知道,服务器(tomcat)会为每一个请求分配一个线程,从filter到controller到service到db再返回,全都都是同一个线程,所以,只要从一开始就把原始请求和这个线程绑定在一起,那么只要在这个线程内,我就能随时拿到这个数据。
是不是很熟悉,这不就是ThreadLocal
嘛!再瞅一眼源代码证明一下?
原文:https://www.cnblogs.com/codervivi/p/14299451.html