【Spring Cloud Gateway】解决TraceId丢失问题
liuian 2025-05-25 14:04 70 浏览
前言
在近期项目安全架构设计中,我们针对请求加解密需求进行了技术方案选型。考虑到未来接口安全策略可能会进行动态调整,最终决定在 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"}
相关推荐
- 宽带登录网站(宽带登录网站怎么登录)
-
在浏览器内输入www.10010.com,进入中国联通网上营业厅后,选择“登录”,输入宽带账号与宽带密码,点击登录即可使用联通宽带登录联通网上营业厅。可通过以下方式办理联通宽带预约服务:1、登录联通网...
- tenda官网登入(tenda官网192.168.0.1登录)
-
腾达无线的路由器登录入口是:tplogin.cn电信运营商定制款登录地址是:192.168.2.1或者192.168.8.12、华为(容易)路由器华为路由器跟荣耀路由器只有IP地址,没有域名,它是...
- u盘格式化不了怎么回事(u盘格式化不了为什么)
-
第一种情况:u盘本身带有写保护开关的情况。这种情况一般是因为有些朋友借的U盘或者不了解U盘本身的情况,不注意打开了写保护开关,导致的U盘写保护,这种情况的解决方法就很简单了,找到开关关掉写保护即可。第...
- windows7专业版sp1补丁(win7旗舰版sp1补丁)
-
win7sp1补丁安装方法如下:1成后,需要打sp1补丁,不过微软对win7的支持已经结束了,建议升级新系统2到微软官网/zh-cn/download找到win7系统版本的补丁,点击下载3勾选对应系统...
- 无线ap面板哪个品牌好(无线ap面板什么品牌好)
-
作为工程商的我,用过用多牌子做无线覆盖工程,用过大品牌的有华为,H3C,思科,比较贵,性能强大。确实是不错的,就是费用高,老板指定就会用的。用过有中高端牌子有很多,但后面用着用着出现问题,不敢用了。现...
-
- 怎么下载07版的office(下载office2007的步骤)
-
office是每个电脑都必备的一个软件,那office2007免费完整版怎么下载呢?下面就来教教大家具体步骤。1、我们在浏览器输入栏搜索“zol”,然后选择官网进入。2、在页面右上角选择“软件下载”,然后选择“软件分类”,点击“办公软件”。...
-
2026-01-09 14:55 liuian
-
- 拼音五笔两用的输入法叫什么
-
这个是因为你电脑设置了五笔为默认输入法,所以你打开的任何窗口都是五笔输入法。你可以在电脑语言设置里面更改默认设置,如win10系统直接在输入法那里打开语言首选项,选择键盘那个选项直接可以按照自己的使用习惯更改默认输入法。可以在不同的窗口设置...
-
2026-01-09 14:05 liuian
-
- 外置sd卡文件加密软件(sd卡加密怎么破解)
-
不管是给手机sd卡加密还是外置sd卡加密,办法都是一样的,具体操作为以下几步:1、在手机【设置】中找到【安全】功能,不同的手机有不一样的选项,有的在【高级设置】中,而有的则在【设置】中。2、在【安全】选项中,找到【设置SD卡密码】或【加密外...
-
2026-01-09 13:55 liuian
- wifi脚本精灵(wifi脚本精灵自动挂机下载安装)
-
1、在应用商店下载脚本精灵,下载安装。2、打开脚本精灵,点击右上角的摄像头图标开始录制。3、如果手机是MIUI系统,则需要开启悬浮窗。(开启方法:按屏幕下方的房子建然后调出后台程序长按脚本精灵的图...
- 万能视频转换器免费版(万能视频转换软件)
-
是收费的不过可以试用,跟没有差不多的“试用”,限时限数的我推荐你使用暴风转码或mediacoder暴风转码虽然功能方面不怎么强大,但其简单易用,而且非常人性化,非常手机化,单从简易性来说,绝对是转手机...
- tp路由器桥接(怎样桥接第二个无线路由器)
-
1、路由器接通电源,打开路由器并链接。2、弹出的界面中,设置管理员密码后跳过引导。3、在应用管理”中找到无线桥接”选项。4、点击开始设置”,找到要连接的无线网络,输入密码后点击下一步”。5、输入无线密...
- 不换电脑怎么提高配置(旧电脑提升配置)
-
1:这个配置总体来说升级的意义不大,已经属于淘汰配置,处理器也不是楼主说的还行,实际上处理器也早已经淘汰,这个配置已经是十年前的配置了。2:对于DDR2内存的老笔记本,4G内存基本上属于已经加满的状态...
- win7旗舰版好还是win8好(win7好还是win7旗舰版好)
-
相比较来说,win7系统更好。1、win8只不过是一个win10的过度系统,使用起来并不是很流畅。2、而win7系统已经经历了十数年的考验,拥有很强大的稳定性和兼容性。3、如果我们是在win7和win...
- 如何修改电脑默认浏览器(如何更改电脑中的默认浏览器)
-
Windows系统:1.打开“设置”应用。2.点击“应用”。3.点击“默认应用”。4.在“网络浏览器”下,选择你想要设置为默认浏览器的应用。macOS系统:1.打开“Safari”浏览器...
- 免费下载360杀毒软件手机版(给我下载一个360杀毒软件)
-
360手机卫士苹果版来自奇虎360推出的一款iPhone手机管理软件,360手机卫士苹果版推为iOS用户提供专业、完善和高效的移动设备管理服务,360手机卫士手机版可以实现流量监控、电池管家、隐私空间...
- 一周热门
-
-
飞牛OS入门安装遇到问题,如何解决?
-
如何在 iPhone 和 Android 上恢复已删除的抖音消息
-
Boost高性能并发无锁队列指南:boost::lockfree::queue
-
大模型手册: 保姆级用CherryStudio知识库
-
用什么工具在Win中查看8G大的log文件?
-
如何在 Windows 10 或 11 上通过命令行安装 Node.js 和 NPM
-
威联通NAS安装阿里云盘WebDAV服务并添加到Infuse
-
Trae IDE 如何与 GitHub 无缝对接?
-
idea插件之maven search(工欲善其事,必先利其器)
-
如何修改图片拍摄日期?快速修改图片拍摄日期的6种方法
-
- 最近发表
- 标签列表
-
- 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)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- vscode切换git分支 (35)
- python bytes转16进制 (35)
- grep前后几行 (34)
- hashmap转list (35)
- c++ 字符串查找 (35)
- mysql刷新权限 (34)
