【Spring Cloud Gateway】解决TraceId丢失问题
liuian 2025-05-25 14:04 59 浏览
前言
在近期项目安全架构设计中,我们针对请求加解密需求进行了技术方案选型。考虑到未来接口安全策略可能会进行动态调整,最终决定在 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"}
相关推荐
- 教你把多个视频合并成一个视频的方法
-
一.情况介绍当你有一个m3u8文件和一个目录,目录中有连续的视频片段,这些片段可以连成一段完整的视频。m3u8文件打开后像这样:m3u8文件,可以理解为播放列表,里面是播放视频片段的顺序。视频片段像这...
- 零代码编程:用kimichat合并一个文件夹下的多个文件
-
一个文件夹里面有很多个srt字幕文件,如何借助kimichat来自动批量合并呢?在kimichat对话框中输入提示词:你是一个Python编程专家,完成如下的编程任务:这个文件夹:D:\downloa...
- Java APT_java APT 生成代码
-
JavaAPT(AnnotationProcessingTool)是一种在Java编译阶段处理注解的工具。APT会在编译阶段扫描源代码中的注解,并根据这些注解生成代码、资源文件或其他输出,...
- Unit Runtime:一键运行 AI 生成的代码,或许将成为你的复制 + 粘贴神器
-
在我们构建了UnitMesh架构之后,以及对应的demo之后,便着手于实现UnitMesh架构。于是,我们就继续开始UnitRuntime,以用于直接运行AI生成的代码。PS:...
- 挣脱臃肿的枷锁:为什么说Vert.x是Java开发者手中的一柄利剑?
-
如果你是一名Java开发者,那么你的职业生涯几乎无法避开Spring。它如同一位德高望重的老国王,统治着企业级应用开发的大片疆土。SpringBoot的约定大于配置、SpringCloud的微服务...
- 五年后,谷歌还在全力以赴发展 Kotlin
-
作者|FredericLardinois译者|Sambodhi策划|Tina自2017年谷歌I/O全球开发者大会上,谷歌首次宣布将Kotlin(JetBrains开发的Ja...
- kotlin和java开发哪个好,优缺点对比
-
Kotlin和Java都是常见的编程语言,它们有各自的优缺点。Kotlin的优点:简洁:Kotlin程序相对于Java程序更简洁,可以减少代码量。安全:Kotlin在类型系统和空值安全...
- 移动端架构模式全景解析:从MVC到MVVM,如何选择最佳设计方案?
-
掌握不同架构模式的精髓,是构建可维护、可测试且高效移动应用的关键。在移动应用开发中,选择合适的软件架构模式对项目的可维护性、可测试性和团队协作效率至关重要。随着应用复杂度的增加,一个良好的架构能够帮助...
- 颜值非常高的XShell替代工具Termora,不一样的使用体验!
-
Termora是一款面向开发者和运维人员的跨平台SSH终端与文件管理工具,支持Windows、macOS及Linux系统,通过一体化界面简化远程服务器管理流程。其核心定位是解决多平台环境下远程连接、文...
- 预处理的底层原理和预处理编译运行异常的解决方案
-
若文章对您有帮助,欢迎关注程序员小迷。助您在编程路上越走越好![Mac-10.7.1LionIntel-based]Q:预处理到底干了什么事情?A:预处理,顾名思义,预先做的处理。源代码中...
- 为“架构”再建个模:如何用代码描述软件架构?
-
在架构治理平台ArchGuard中,为了实现对架构的治理,我们需要代码+模型描述所要处理的内容和数据。所以,在ArchGuard中,我们有了代码的模型、依赖的模型、变更的模型等,剩下的两个...
- 深度解析:Google Gemma 3n —— 移动优先的轻量多模态大模型
-
2025年6月,Google正式发布了Gemma3n,这是一款能够在2GB内存环境下运行的轻量级多模态大模型。它延续了Gemma家族的开源基因,同时在架构设计上大幅优化,目标是让...
- 比分网开发技术栈与功能详解_比分网有哪些
-
一、核心功能模块一个基本的比分网通常包含以下模块:首页/总览实时比分看板:滚动展示所有正在进行的比赛,包含比分、比赛时间、红黄牌等关键信息。热门赛事/焦点战:突出显示重要的、关注度高的比赛。赛事导航...
- 设计模式之-生成器_一键生成设计
-
一、【概念定义】——“分步构建复杂对象,隐藏创建细节”生成器模式(BuilderPattern):一种“分步构建型”创建型设计模式,它将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建...
- 构建第一个 Kotlin Android 应用_kotlin简介
-
第一步:安装AndroidStudio(推荐IDE)AndroidStudio是官方推荐的Android开发集成开发环境(IDE),内置对Kotlin的完整支持。1.下载And...
- 一周热门
-
-
【验证码逆向专栏】vaptcha 手势验证码逆向分析
-
Psutil + Flask + Pyecharts + Bootstrap 开发动态可视化系统监控
-
一个解决支持HTML/CSS/JS网页转PDF(高质量)的终极解决方案
-
再见Swagger UI 国人开源了一款超好用的 API 文档生成框架,真香
-
网页转成pdf文件的经验分享 网页转成pdf文件的经验分享怎么弄
-
C++ std::vector 简介
-
飞牛OS入门安装遇到问题,如何解决?
-
系统C盘清理:微信PC端文件清理,扩大C盘可用空间步骤
-
10款高性能NAS丨双十一必看,轻松搞定虚拟机、Docker、软路由
-
python使用fitz模块提取pdf中的图片
-
- 最近发表
- 标签列表
-
- 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)