OpenFeign 调用未携带 Cookie:源码分析与拦截器配置
在 Spring Boot 2.1.8、OpenFeign 2.1.3 学习环境中分析 Cookie 未随服务调用传递的问题,整理调用链、配置方法和上下文范围。
一 背景
最近在学习大型分布式商城系统时,用到了feign远程调用。集群会话使用的是spring session,使用ThreadLocal保存当前用户登录状态,在一次使用feign调用中,在被调用的服务(购物车服务)中无法获取到当前线程用户数据(从订单服务调用购物车服务),使用断点检查发现,购物车服务的请求头中并没有包含对应的会话信息(cookie)。
springboot版本为2.1.8
openfeign版本为2.1.3 不同版本源码会有差异,但大致流程应差不多。
二 分析
1.业务流程
1. 流程图

2. feign源码分析
第一步进入
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (!"equals".equals(method.getName())) {
if ("hashCode".equals(method.getName())) {
return this.hashCode();
} else {
return "toString".equals(method.getName()) ? this.toString() :
//真正执行模块,其他都是一些判断
((MethodHandler)this.dispatch.get(method)).invoke(args);
}
} else {
try {
Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return this.equals(otherHandler);
} catch (IllegalArgumentException var5) {
return false;
}
}
}
第二步
public Object invoke(Object[] argv) throws Throwable {
//构建一个新的requestTemplate,该request请求头为空
RequestTemplate template = this.buildTemplateFromArgs.create(argv);
Retryer retryer = this.retryer.clone();
while(true) {
try {
//执行请求块,使用上面创建的requestTemplate进行请求
return this.executeAndDecode(template);
} catch (RetryableException var8) {
.......省略部分源码
}
}
}
第三步: 查看如何创建新的requestTemplate
public RequestTemplate create(Object[] argv) {
//this.metadata.template() 从元数据中获取一个template,实际就是new RequestTemplate()
//RequestTemplate.from() 对默认的template进行处理,返回处理后的requestTemplate
RequestTemplate mutable = RequestTemplate.from(this.metadata.template());
if (this.metadata.urlIndex() != null) {
int urlIndex = this.metadata.urlIndex();
Util.checkArgument(argv[urlIndex] != null, "URI parameter %s was null", new Object[]{urlIndex});
mutable.target(String.valueOf(argv[urlIndex]));
}
......省略部分源码
}
第四步:查看处理逻辑 RequestTemplate.from(this.metadata.template())
public static RequestTemplate from(RequestTemplate requestTemplate) {
//通过原requestTemplate的参数生成一个新的RequestTemplate
RequestTemplate template = new RequestTemplate(requestTemplate.target, requestTemplate.fragment, requestTemplate.uriTemplate, requestTemplate.method, requestTemplate.charset, requestTemplate.body, requestTemplate.decodeSlash, requestTemplate.collectionFormat);
//处理传入进来requestTemplate的查询参数
if (!requestTemplate.queries().isEmpty()) {
template.queries.putAll(requestTemplate.queries);
}
//处理传入进来requestTemplate的请求头,如果不为空,就放到新的template中去
if (!requestTemplate.headers().isEmpty()) {
template.headers.putAll(requestTemplate.headers);
}
return template;
}
由第四步源码可知,其请求头和查询参数都是在第三步从元数据中获取的RequestTemplate中来的。因此,具体分析第三步中的this.metadata.template(),发现template的无参构造方法如下
public RequestTemplate() {
//也是空的
this.headers = new TreeMap(String.CASE_INSENSITIVE_ORDER);
this.resolved = false;
this.charset = Util.UTF_8;
this.body = Body.empty();
this.decodeSlash = true;
this.collectionFormat = CollectionFormat.EXPLODED;
}
该调用链说明入站 HttpServletRequest 的 Cookie 不会仅因创建 RequestTemplate 就自动复制过去。不能由默认构造器推导所有 Feign 请求都没有请求头;声明式配置与拦截器也可能添加请求头。
三 处理
1. 分析feign的执行方法
this.executeAndDecode(template),执行和解码
Object executeAndDecode(RequestTemplate template) throws Throwable {
//将template处理成request
Request request = this.targetRequest(template);
if (this.logLevel != Level.NONE) {
this.logger.logRequest(this.metadata.configKey(), this.logLevel, request);
}
......省略部分源码
}
this.targetRequest(template)//获取目标request
Request targetRequest(RequestTemplate template) {
//获取request拦截器(默认情况下,是空的)
Iterator var2 = this.requestInterceptors.iterator();
while(var2.hasNext()) {
//遍历拦截器,执行拦截器的apply方法
RequestInterceptor interceptor = (RequestInterceptor)var2.next();
interceptor.apply(template);
}
return this.target.apply(template);
}
RequestInterceptor是一个接口,interceptor.apply(template) 也是一个抽象方法,参考BasicAuthRequestInterceptor实现的方法
public class BasicAuthRequestInterceptor implements RequestInterceptor {
private final String headerValue;
......省略部分源码
//RequestInterceptor拦截器的apply方法,可以修改template的请求头
public void apply(RequestTemplate template) {
template.header("Authorization", new String[]{this.headerValue});
}
}
总结: 我们在RequestInterceptor中加入自定义的request拦截器,这样就能修改请求头了。
2. 处理
-
先看分析中this.requestInterceptors(request拦截器是如何获取的)
//省略部分源码,只看重要部分 final class SynchronousMethodHandler implements MethodHandler { //私有属性 private final List<RequestInterceptor> requestInterceptors; //在构造的时候会传入request拦截器 private SynchronousMethodHandler(Target<?> target, Client client, Retryer retryer, this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors for %s", target); } //静态内部类 Factory static class Factory { ...... private final List<RequestInterceptor> requestInterceptors; ..... Factory(Client client, Retryer retryer, List<RequestInterceptor> requestInterceptors, Logger logger, Logger.Level logLevel, boolean decode404, boolean closeAfterDecode, ExceptionPropagationPolicy propagationPolicy) { .... this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors"); .... } //有一个创建MethodHandler方法 public MethodHandler create(Target<?> target, MethodMetadata md, RequestTemplate.Factory buildTemplateFromArgs, Options options, Decoder decoder, ErrorDecoder errorDecoder) { return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger, logLevel, md, buildTemplateFromArgs, options, decoder, errorDecoder, decode404, closeAfterDecode, propagationPolicy); } } -
查看谁在调用Factory
public Feign build() { //此处没有传参,requestInterceptors来自Builder SynchronousMethodHandler.Factory synchronousMethodHandlerFactory = new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger, logLevel, decode404, closeAfterDecode, propagationPolicy); ParseHandlersByName handlersByName = new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder, errorDecoder, synchronousMethodHandlerFactory); return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder); } -
查看Builder
public static class Builder { private final List<RequestInterceptor> requestInterceptors = new ArrayList<RequestInterceptor>(); ......省略部分源码 /** * Adds a single request interceptor to the builder. */ public Builder requestInterceptor(RequestInterceptor requestInterceptor) { this.requestInterceptors.add(requestInterceptor); return this; } /** * Sets the full set of request interceptors for the builder, overwriting any previous * interceptors. */ public Builder requestInterceptors(Iterable<RequestInterceptor> requestInterceptors) { this.requestInterceptors.clear(); for (RequestInterceptor requestInterceptor : requestInterceptors) { this.requestInterceptors.add(requestInterceptor); } return this; } } -
通过Builder requestInterceptor(RequestInterceptor requestInterceptor)方法分析到,RequestInterceptor是从容器中获取的。详细源码查阅即知
3.配置
查看RequestInterceptor接口的注释可知,可以配置零个或多个RequestInterceptor拦截器,通过apply方法修改RequestTemplate。因此自定义一个request拦截器实现功能即可。
import feign.RequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
// 由指定 @FeignClient 的 configuration 引用,不放入全局组件扫描。
public class CartFeignConfiguration {
@Bean
public RequestInterceptor cookieRelay() {
return template -> {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
if (!(attributes instanceof ServletRequestAttributes)) {
return;
}
String cookie = ((ServletRequestAttributes) attributes)
.getRequest().getHeader("Cookie");
if (cookie != null && !cookie.trim().isEmpty()) {
template.header("Cookie", cookie);
}
};
}
}
将拦截器限制在可信客户端
在需要会话传递的内部客户端上指定 @FeignClient(name = "cart-service", configuration = CartFeignConfiguration.class)。这里的完整 Cookie 透传只适用于已经明确需要共享会话的可信内部调用;更窄的业务契约应仅传必要身份信息。第三方客户端不应继承该配置。
RequestContextHolder 与线程关联。定时任务没有 HTTP 请求、异步执行切换线程时,都不能假定这里一定存在原请求;示例选择不复制,而不是抛空指针异常。是否需要显式传递身份,应由调用协议决定,不能靠全局继承线程变量掩盖边界。
本次补充了空上下文和空 Cookie 判断,但未在原有两个服务中重新执行会话测试。源码仍按原记录的 Spring Boot 2.1.8、OpenFeign 2.1.3 阅读,不宣称新版内部类结构相同。
- 本文标签: Spring Boot
- 本文链接: https://www.tianyajuanke.top/article/71
- 版权声明: 本文由吴沛芙原创发布,转载请遵循《署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)》许可协议授权