百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT知识 > 正文

【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框架,,这是一种基于数据流和变化传播的编程范式。在响应式编程中,数据被视为一种流,可以异步地、非阻塞地处理。这种模型能够更好地利用系统资源,提高应用程序的吞吐量和响应性。

  1. 异步非阻塞I/O WebFlux基于Reactor库实现,该库遵循Reactive Streams规范,提供高效的异步非阻塞数据处理能力。通过Reactor的Mono和Flux类型,WebFlux能够以数据流的形式处理请求与响应,实现完全的异步编程模型。
  2. 线程模型 WebFlux默认集成Netty作为底层服务器框架。Netty采用异步事件驱动架构,通过少量线程即可支撑高并发连接。其核心的事件循环(EventLoop)机制负责处理网络Channel的全生命周期事件,包括连接建立、数据读写等关键操作。
  3. 请求处理流程
  4. 请求接收:客户端请求到达时,Netty的EventLoop将其封装为ServerHttpRequest对象
  5. 请求处理:WebFlux路由将请求递交给对应处理器,处理器返回Mono或Flux表示异步处理结果
  6. 响应生成:处理器完成业务逻辑后,构建Mono或Flux响应流
  7. 响应发送:WebFlux将响应流回传给Netty,由EventLoop线程负责最终的响应输出
  8. 线程切换 在上述过程中,请求的接收和响应的发送是由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的步骤)
怎么下载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卡加密怎么破解)

不管是给手机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手机卫士手机版可以实现流量监控、电池管家、隐私空间...