Feign调用服务时丢失Cookie和Header信息的解决方案
作者:迷雾总会解
Feign调用服务丢失Cookie和Header信息
今天在使用Feign调用其他微服务的接口时,发现了一个问题:因为我的项目采用了无状态登录,token信息是存放在cookie中的,所以调用接口时,因为cookie中没有token信息,我的请求被拦截器拦截了。
参考几篇文章,靠谱的解决方法是:将cookie信息放到请求头中,再进行调用接口时,拦截器中可以对请求头进行解析,获取cookie信息
服务调用方
package top.codekiller.manager.upload.config; import feign.RequestInterceptor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import java.util.Enumeration; /** * @author codekiller * @date 2020/5/26 14:22 * * 自定义的请求头处理类,处理服务发送时的请求头; * 将服务接收到的请求头中的uniqueId和token字段取出来,并设置到新的请求头里面去转发给下游服务 * 比如A服务收到一个请求,请求头里面包含uniqueId和token字段,A处理时会使用Feign客户端调用B服务 * 那么uniqueId和token这两个字段就会添加到请求头中一并发给B服务; */ @Configuration @Slf4j public class FeignHeaderConfiguration { @Bean public RequestInterceptor requestInterceptor() { return requestTemplate -> { ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attrs != null) { HttpServletRequest request = attrs.getRequest(); // 如果在Cookie内通过如下方式取 Cookie[] cookies = request.getCookies(); if (cookies != null && cookies.length > 0) { for (Cookie cookie : cookies) { requestTemplate.header(cookie.getName(), cookie.getValue()); System.out.println("信息"+cookie.getName()+cookie.getValue()); } } else { log.warn("FeignHeadConfiguration", "获取Cookie失败!"); } // 如果放在header内通过如下方式取 Enumeration<String> headerNames = request.getHeaderNames(); if (headerNames != null) { while (headerNames.hasMoreElements()) { String name = headerNames.nextElement(); String value = request.getHeader(name); /** * 遍历请求头里面的属性字段,将jsessionid添加到新的请求头中转发到下游服务 * */ if ("jsessionid".equalsIgnoreCase(name)) { log.debug("添加自定义请求头key:" + name + ",value:" + value); requestTemplate.header(name, value); } else { log.debug("FeignHeadConfiguration", "非自定义请求头key:" + name + ",value:" + value + "不需要添加!"); } } } else { log.warn("FeignHeadConfiguration", "获取请求头失败!"); } } }; } }
服务接受方
//有些请求时从通过feign进行请求的,这一部分请求时不包含cookie信息的,因此我们要从请求头中获取 Enumeration<String> headerNames = request.getHeaderNames(); if (headerNames != null) { while (headerNames.hasMoreElements()) { String name = headerNames.nextElement(); String value = request.getHeader(name); System.out.println("header的信息"+name+"::::"+value); if(name.equalsIgnoreCase("MC_TOKEN")){ //注意这里变成了小写 token=value; } } }
运行的时候,我发现请求还是被拦截了,看了下打印信息,发现我的MC_TOKEN变成了小写,所以在字符串进行比较的时候要忽略大小写。
以下为扩展,仅仅记录一下
这样仍然有个问题:
在开启熔断器之后,方法里的attrs是null,因为熔断器默认的隔离策略是thread,也就是线程隔离,实际上接收到的对象和这个在发送给B不是一个线程,怎么办?
有一个办法,修改隔离策略hystrix.command.default.execution.isolation.strategy=SEMAPHORE,改为信号量的隔离模式,但是不推荐,因为thread是默认的,而且要命的是信号量模式,熔断器不生效,比如设置了熔断时间。
另一个办法:重写Feign的隔离策略
import com.netflix.hystrix.HystrixThreadPoolKey; import com.netflix.hystrix.HystrixThreadPoolProperties; import com.netflix.hystrix.strategy.HystrixPlugins; import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy; import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariable; import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableLifecycle; import com.netflix.hystrix.strategy.eventnotifier.HystrixEventNotifier; import com.netflix.hystrix.strategy.executionhook.HystrixCommandExecutionHook; import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisher; import com.netflix.hystrix.strategy.properties.HystrixPropertiesStrategy; import com.netflix.hystrix.strategy.properties.HystrixProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Callable; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * 自定义Feign的隔离策略; * 在转发Feign的请求头的时候,如果开启了Hystrix,Hystrix的默认隔离策略是Thread(线程隔离策略),因此转发拦截器内是无法获取到请求的请求头信息的,可以修改默认隔离策略为信号量模式:hystrix.command.default.execution.isolation.strategy=SEMAPHORE,这样的话转发线程和请求线程实际上是一个线程,这并不是最好的解决方法,信号量模式也不是官方最为推荐的隔离策略;另一个解决方法就是自定义Hystrix的隔离策略,思路是将现有的并发策略作为新并发策略的成员变量,在新并发策略中,返回现有并发策略的线程池、Queue;将策略加到Spring容器即可; * */ @Component public class FeignHystrixConcurrencyStrategyIntellif extends HystrixConcurrencyStrategy { private static final Logger log = LoggerFactory.getLogger(FeignHystrixConcurrencyStrategyIntellif.class); private HystrixConcurrencyStrategy delegate; public FeignHystrixConcurrencyStrategyIntellif() { try { this.delegate = HystrixPlugins.getInstance().getConcurrencyStrategy(); if (this.delegate instanceof FeignHystrixConcurrencyStrategyIntellif) { // Welcome to singleton hell... return; } HystrixCommandExecutionHook commandExecutionHook = HystrixPlugins.getInstance().getCommandExecutionHook(); HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance().getEventNotifier(); HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher(); HystrixPropertiesStrategy propertiesStrategy = HystrixPlugins.getInstance().getPropertiesStrategy(); this.logCurrentStateOfHystrixPlugins(eventNotifier, metricsPublisher, propertiesStrategy); HystrixPlugins.reset(); HystrixPlugins.getInstance().registerConcurrencyStrategy(this); HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook); HystrixPlugins.getInstance().registerEventNotifier(eventNotifier); HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher); HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy); } catch (Exception e) { log.error("Failed to register Sleuth Hystrix Concurrency Strategy", e); } } private void logCurrentStateOfHystrixPlugins(HystrixEventNotifier eventNotifier, HystrixMetricsPublisher metricsPublisher, HystrixPropertiesStrategy propertiesStrategy) { if (log.isDebugEnabled()) { log.debug("Current Hystrix plugins configuration is [" + "concurrencyStrategy [" + this.delegate + "]," + "eventNotifier [" + eventNotifier + "]," + "metricPublisher [" + metricsPublisher + "]," + "propertiesStrategy [" + propertiesStrategy + "]," + "]"); log.debug("Registering Sleuth Hystrix Concurrency Strategy."); } } @Override public <T> Callable<T> wrapCallable(Callable<T> callable) { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); return new WrappedCallable<>(callable, requestAttributes); } @Override public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey, HystrixProperty<Integer> corePoolSize, HystrixProperty<Integer> maximumPoolSize, HystrixProperty<Integer> keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { return this.delegate.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); } @Override public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey, HystrixThreadPoolProperties threadPoolProperties) { return this.delegate.getThreadPool(threadPoolKey, threadPoolProperties); } @Override public BlockingQueue<Runnable> getBlockingQueue(int maxQueueSize) { return this.delegate.getBlockingQueue(maxQueueSize); } @Override public <T> HystrixRequestVariable<T> getRequestVariable(HystrixRequestVariableLifecycle<T> rv) { return this.delegate.getRequestVariable(rv); } static class WrappedCallable<T> implements Callable<T> { private final Callable<T> target; private final RequestAttributes requestAttributes; public WrappedCallable(Callable<T> target, RequestAttributes requestAttributes) { this.target = target; this.requestAttributes = requestAttributes; } @Override public T call() throws Exception { try { RequestContextHolder.setRequestAttributes(requestAttributes); return target.call(); } finally { RequestContextHolder.resetRequestAttributes(); } } } }
然后使用默认的熔断器隔离策略,也可以在拦截器内获取到上游服务的请求头信息了;
Feign调用存在的问题
① feign远程调用丢失请求头
问题描述:
当远程调用其他服务时,设置了拦截器判断用户是否登录,但是结果是即使用户登录了,也会显示用户没登录,原因在于远程调用时,发送的请求是一个新的情求,请求中并不存在cookie,而原始请求中是携带cookie的。
解决方案如下:
@Configuration public class MallFeignConfig { @Bean("requestInterceptor") public RequestInterceptor requestInterceptor() { RequestInterceptor requestInterceptor = template -> { //1、使用RequestContextHolder拿到刚进来的请求数据 ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (requestAttributes != null) { //老请求 HttpServletRequest request = requestAttributes.getRequest(); if (request != null) { //2、同步请求头的数据(主要是cookie) //把老请求的cookie值放到新请求上来,进行一个同步 String cookie = request.getHeader("Cookie"); template.header("Cookie", cookie); } } }; return requestInterceptor; } }
② 异步调用Feign丢失上下文问题
问题描述:
由于feign请求拦截器为新的request设置请求头底层是使用ThreadLocal保存刚进来的请求,所以在异步情况下,其他线程并不能获取到主线程的ThreadLocal,所以也拿不到请求。
解决:
先获取主线程的requestAttributes,再分别向其他线程中设置
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); CompletableFuture.runAsync(() ->{ RequestContextHolder.setRequestAttributes(requestAttributes); });
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。