【Spring Cloud Gateway】解决TraceId丢失问题
liuian 2025-05-25 14:04 3 浏览
前言
在近期项目安全架构设计中,我们针对请求加解密需求进行了技术方案选型。考虑到未来接口安全策略可能会进行动态调整,最终决定在 API 网关层实现统一的加解密模块。
方案实施过程中发现一个关键问题:当请求体经过加解密过滤器处理后,会导致调用链追踪标识(traceId)丢失。
问题排查
全局过滤器实现实现 GlobalFilter 接口,在 filter 方法中对响应体进行加密
public class RespEncryptFilter implements GatewayFilter, Ordered {
private int order;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String bgDebug = exchange.getAttributeOrDefault(ConstantFilter.BG_DEBUG_KEY, ConstantFilter.REQ_RES_ENCRYPT);
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.buffer().map(dataBuffer -> {
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffer);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
//释放掉内存
DataBufferUtils.release(join);
// 正常返回的数据
String rootData = new String(content, Charset.forName("UTF-8"));
byte[] respData = rootData.getBytes();
if(ConstantFilter.REQ_RES_ENCRYPT.equals(bgDebug)){
// 对数据进行加密
String randomKey = AESUtil.getRandomKey();
String encryptData = AESUtil.AESEncrypt(rootData, randomKey, "CBC");
String encryptRandomKey = RSAUtils.publicEncrypt(randomKey);
JSONObject json = new JSONObject();
json.put("k", encryptRandomKey);
json.put("v", encryptData);
log.info("加密后数据:{}",json.toJSONString())
respData = json.toJSONString().getBytes();
}
// 加密后的数据返回给客户端
byte[] uppedContent = new String(respData, Charset.forName("UTF-8")).getBytes();
return bufferFactory.wrap(uppedContent);
}));
}
return super.writeWith(body);
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
@Override
public int getOrder() {
return this.order;
}
public AppRespEncryptFilter(int order){
this.order = order;
}
}
我的日志格式如下:
<property name="pattern">[TRACEID=%X{traceId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M()/%L - %msg%xEx%n</property>
查看打印的日志,发现 TRACEID 并没有打印:
[TRACEID=]12:56:22.123-INFO 加密后数据:{"k":"xxxxx","v":"xxxxx"}
我们先了解下 TRACEID 打印的原理
traceId 的打印主要涉及MDC(Mapped Diagnostic Context)机制和日志框架的配合使用。
MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 、logback及log4j2 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。
也就是说TRACEID 是和线程绑定的。
我们再看下gateway的原理
Spring Cloud Gateway,它基于WebFlux,WebFlux是Spring Framework 5.0中引入的响应式Web框架,,这是一种基于数据流和变化传播的编程范式。在响应式编程中,数据被视为一种流,可以异步地、非阻塞地处理。这种模型能够更好地利用系统资源,提高应用程序的吞吐量和响应性。
- 异步非阻塞I/O WebFlux基于Reactor库实现,该库遵循Reactive Streams规范,提供高效的异步非阻塞数据处理能力。通过Reactor的Mono和Flux类型,WebFlux能够以数据流的形式处理请求与响应,实现完全的异步编程模型。
- 线程模型 WebFlux默认集成Netty作为底层服务器框架。Netty采用异步事件驱动架构,通过少量线程即可支撑高并发连接。其核心的事件循环(EventLoop)机制负责处理网络Channel的全生命周期事件,包括连接建立、数据读写等关键操作。
- 请求处理流程
- 请求接收:客户端请求到达时,Netty的EventLoop将其封装为ServerHttpRequest对象
- 请求处理:WebFlux路由将请求递交给对应处理器,处理器返回Mono或Flux表示异步处理结果
- 响应生成:处理器完成业务逻辑后,构建Mono或Flux响应流
- 响应发送:WebFlux将响应流回传给Netty,由EventLoop线程负责最终的响应输出
- 线程切换 在上述过程中,请求的接收和响应的发送是由Netty的EventLoop线程负责的,而请求和响应处理则可能由不同的线程来执行,这取决于处理逻辑中是否涉及线程切换。例如,如果处理逻辑中调用了阻塞的I/O操作,WebFlux会将这个操作切换到另一个线程池中执行,以避免阻塞EventLoop线程。
从上面了解到gateway 求的接收和响应的发送是由Netty的EventLoop线程负责的,也就是请求和响应处理可能不是同一个线程,这就是导致MDC 中的TRACEID丢失的原因。
解决方案
我们可以按如下方式将 TRACEID 放入head请求头中,如下:
public class RespEncryptFilter implements GatewayFilter, Ordered {
private int order;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String bgDebug = exchange.getAttributeOrDefault(ConstantFilter.BG_DEBUG_KEY, ConstantFilter.REQ_RES_ENCRYPT);
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
//从请求头中取出traceId,放入MDC中
MDC.put("traceId",getHeaders().getFirst("traceId"));
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.buffer().map(dataBuffer -> {
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffer);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
//释放掉内存
DataBufferUtils.release(join);
// 正常返回的数据
String rootData = new String(content, Charset.forName("UTF-8"));
byte[] respData = rootData.getBytes();
if(ConstantFilter.REQ_RES_ENCRYPT.equals(bgDebug)){
// 对数据进行加密
String randomKey = AESUtil.getRandomKey();
String encryptData = AESUtil.AESEncrypt(rootData, randomKey, "CBC");
String encryptRandomKey = RSAUtils.publicEncrypt(randomKey);
JSONObject json = new JSONObject();
json.put("k", encryptRandomKey);
json.put("v", encryptData);
log.info("加密后数据:{}",json.toJSONString())
respData = json.toJSONString().getBytes();
}
// 加密后的数据返回给客户端
byte[] uppedContent = new String(respData, Charset.forName("UTF-8")).getBytes();
return bufferFactory.wrap(uppedContent);
}));
}
return super.writeWith(body);
}
};
//从MDC取出traceId,放入请求头Head中
decoratedResponse.getHeaders.add("traceId",MDC.get("traceId"));
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
@Override
public int getOrder() {
return this.order;
}
public AppRespEncryptFilter(int order){
this.order = order;
}
}
验证日志打印如下:
[TRACEID=1324323234884848882]12:56:22.123-INFO 加密后数据:{"k":"xxxxx","v":"xxxxx"}
相关推荐
- 如何修改图片拍摄日期?快速修改图片拍摄日期的6种方法
-
在数字化时代,图像作为信息传递的重要载体,在个人生活记录、新闻传播及商业营销中发挥着不可替代的作用。然而,当面对特定需求时,如隐私保护、编辑优化或时间戳校正等场景,调整图片拍摄时间的需求时常出现。通过...
- python教程从基础到精通,第9课—日期与时间
-
Hello,小伙伴们,祝大家五.一玩得快乐!刚学习完了七大数据类型,今天咱们来学习日期与时间的表示方法。Python标准库中提供了时间和日期的支持:calendar:日历相关;time、datetim...
- Python中datetime模块和date类的主要区别是什么?
-
Python中datetime模块和date类的主要区别如下:一、功能范围差异datetime模块核心功能:提供完整的日期和时间处理能力,包含日期、时间、时间间隔、时区等操作。关键类:datetime...
- 解密Python时间测量迷雾:高精度计时器time.perf_counter的妙用
-
当我们在Python中使用time模块进行时间测量时,可能会遇到一些精度不够的问题。具体而言,time.time()返回的是自纪元以来的秒数,但在一些情况下,其精度可能受到系统硬件时钟的限制,无法捕捉...
- Python技能:时间管理哪家强?time、datetime、calendar来相会!
-
大家好,我是钢铁老豆!快到五一了,每年到了这个时间点,就又该吐槽放假调休啦!真心不如不调,心累啊!言归正传,今天我们要聊聊Python是如何操作日期和时间的。0.模块简介在Python中,处理日期和...
- python之时间处理
-
datetime包导入包与模块fromdatetimeimportdatetimeimportdatetime常用函数函数名功能说明now获取当前时间戳用法:now=datetime.n...
- 软件测试|教你轻松玩转Python日期时间
-
Python基础之日期时间处理前言:软件测试工作中,有时会需要我们在代码中处理日期以及时间,python内置的datetime模块就可以很好地帮我们处理这个问题。该模块常用的类有:类名功能说明date...
- 「耗时测试」python time包中的time()和process_time()如何选择?
-
在统计python代码执行速度时要使用到time包,可以使用time.time()和time.process_time()(注:Python3.8已移除clock()方法,可以使用perf_...
- python进阶突破内置模块——日期与时间详解
-
Python提供了多个内置模块用于处理日期和时间,涵盖了从基础时间操作到时区管理的各种需求。以下是核心模块及其关键功能的详细说明:1.datetime模块datetime是处理日期和时间的核心模块...
- python就该这么学:python快速获取系统时间
-
在python语言中,为了得到一定目的,多数通过调用第三方的库来完成。要获取系统时间需要调用时间相关的库time。通过importtime来引入库。为了方便编码或者防止歧义,也可以通过import...
- Python日期和时间
-
说明Python提供了一个time和calendar模块可以用于格式化日期和时间。时间间隔是以秒为单位的浮点小数。每个时间戳都以自从1970年1月1日午夜(历元)经过了多长时间来表示。pyt...
- python内置时间函数time详解
-
内置函数时间time()1、年:tm_year,月:tm_mon,日:tm_mday,时:tm_hour,分:tm_min,秒:tm_sec,星期:tm_wday(从0开始)2、Time.t...
- 【Python数据分析系列】将一个时间戳转换为可读的日期和时间格式
-
这是我的第396篇原创文章。一、引言在Python中可以通过datetime模块来实现。一般来说,时间戳通常是自1970年1月1日(称为“Unix时间”)以来的秒数。以下是一个示例,演示如何将这...
- 程序员的日常:时间戳和时区的故事
-
什么是时间戳(timestamp)?它和时区(timezone)又有什么关系?初学者可能一开始很难搞懂时间戳这个概念,就像这期《程序员的日常》漫画中的主人公一样。漫画注释从漫画中举的例子来看,这里的时...
- 快速掌握Python时间函数的常用知识
-
我们经常要用到时间,像日志log就要记录时间,什么时候做了什么事情;什么时候调用了哪些过程;什么时候返回了错误等等。时间模块里面的一些方法也是经常会用到的,比如游戏要控制时间,如贪吃蛇的移动时间控制,...
- 一周热门
-
-
Python实现人事自动打卡,再也不会被批评
-
Psutil + Flask + Pyecharts + Bootstrap 开发动态可视化系统监控
-
一个解决支持HTML/CSS/JS网页转PDF(高质量)的终极解决方案
-
【验证码逆向专栏】vaptcha 手势验证码逆向分析
-
再见Swagger UI 国人开源了一款超好用的 API 文档生成框架,真香
-
网页转成pdf文件的经验分享 网页转成pdf文件的经验分享怎么弄
-
C++ std::vector 简介
-
python使用fitz模块提取pdf中的图片
-
《人人译客》如何规划你的移动电商网站(2)
-
Jupyterhub安装教程 jupyter怎么安装包
-
- 最近发表
-
- 如何修改图片拍摄日期?快速修改图片拍摄日期的6种方法
- python教程从基础到精通,第9课—日期与时间
- Python中datetime模块和date类的主要区别是什么?
- 解密Python时间测量迷雾:高精度计时器time.perf_counter的妙用
- Python技能:时间管理哪家强?time、datetime、calendar来相会!
- python之时间处理
- 软件测试|教你轻松玩转Python日期时间
- 「耗时测试」python time包中的time()和process_time()如何选择?
- python进阶突破内置模块——日期与时间详解
- python就该这么学:python快速获取系统时间
- 标签列表
-
- python判断字典是否为空 (50)
- crontab每周一执行 (48)
- aes和des区别 (43)
- bash脚本和shell脚本的区别 (35)
- canvas库 (33)
- dataframe筛选满足条件的行 (35)
- gitlab日志 (33)
- lua xpcall (36)
- blob转json (33)
- python判断是否在列表中 (34)
- python html转pdf (36)
- 安装指定版本npm (37)
- idea搜索jar包内容 (33)
- css鼠标悬停出现隐藏的文字 (34)
- linux nacos启动命令 (33)
- gitlab 日志 (36)
- adb pull (37)
- table.render (33)
- uniapp textarea (33)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- react-admin (33)
- vscode切换git分支 (35)
- python bytes转16进制 (35)
- grep前后几行 (34)