Gateway网关拦截自定义header & 用户上下文打通实战

在微服务架构中,如何在不同服务之间安全、高效地传递用户身份信息是一个常见且重要的需求。本文将详细介绍如何通过Spring Cloud Gateway全局过滤器 拦截并处理自定义header,并结合ThreadLocal实现用户上下文在微服务链路上的无缝传递

一、背景与挑战

在微服务拆分后,一个业务请求往往需要经过多个服务。每个服务都可能需要获取当前登录用户的信息(如用户ID、角色、权限等)。如果每次都在业务代码中从请求头中解析,会导致代码重复、维护困难,并可能引发线程安全问题。

常见的解决方案是:

  1. 网关统一鉴权:在网关层进行身份认证,将用户信息注入请求头。

  2. 服务内上下文传递:通过拦截器提取header中的用户信息,存入ThreadLocal中,供业务代码使用。

二、整体链路流程

以下是完整的用户信息传递链路图:

三、详细设计与实现

以下是完整的详细设计链路图:

3.1 Gateway全局过滤器

在网关层,我们实现一个GlobalFilter,用于拦截所有请求,解析token,并将用户信息注入header中向下游传递。

复制代码
package cn.com.zcits.gateway.filter;

import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import com.google.gson.Gson;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * Gateway全局过滤器
 */
@Component
@Slf4j
public class GatewayGlobalFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获取当前请求对象,包含HTTP请求的所有信息
        ServerHttpRequest request = exchange.getRequest();
        // 创建请求构建器,用于修改请求参数、头部等信息
        ServerHttpRequest.Builder mutate = request.mutate();
        // 获取请求的URL路径,用于路由判断
        String url = request.getURI().getPath();
        // 从请求头中获取token信息,用于身份验证
        String token = request.getHeaders().getFirst("token");
        // 记录请求URL和token信息到日志,便于问题排查
        log.info("GatewayGlobalFilter url: {}, token: {}", url, token);
        // TODO 可以做拦截放行
        // 如果是登录接口,则直接放行不进行token验证等后续处理
        if (url.equals("/user/doLogin")) {
            return chain.filter(exchange);
        }
        // 为请求添加一个测试头部信息
        mutate.header("test", "123456");
        // 将获取到的测试token添加到请求头中,供下游服务测试使用
        mutate.header("testToken", token);
        //测试使用
        mutate.header("loginId", "cest-loginId");
        // 构建全新的请求交换对象,包含修改后的请求信息
        ServerWebExchange webExchange = exchange.mutate()
                .request(mutate.build())
                .build();
        // 继续执行过滤器链,将请求传递给下一个过滤器或目标服务
        return chain.filter(webExchange);
    }

}

3.2. 微服务登录拦截器

在微服务内部,通过Spring MVC拦截器获取header中的用户信息,并存入ThreadLocal上下文。

复制代码
package cn.com.zcits.intercept;

import cn.com.context.LoginContextHolder;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.Nullable;
import org.springframework.web.servlet.HandlerInterceptor;


/**
 * 登录拦截器
 */
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String loginId = request.getHeader("loginId");
        if (StringUtils.isNotBlank(loginId)) {
            LoginContextHolder.set("loginId", loginId);
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
        // 请求完成后清理ThreadLocal,防止内存泄漏
        LoginContextHolder.remove();
    }

}

3.3 MVC全局配置

将拦截器注册到Spring MVC中,使其对所有请求生效。

复制代码
package cn.com.zcits.config;

import cn.com.zcits.intercept.LoginInterceptor;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

/**
 * mvc的全局处理
 */
@Configuration
public class GlobalConfig implements WebMvcConfigurer {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        //super.configureMessageConverters(converters);
        converters.add(mappingJackson2HttpMessageConverter());
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**");
    }

    /**
     * 自定义mappingJackson2HttpMessageConverter
     * 目前实现:空值忽略,空字段可返回
     */
    private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        return new MappingJackson2HttpMessageConverter(objectMapper);
    }


}

3.4 用户上下文工具类(Common模块)

提供一个基于InheritableThreadLocal的上下文持有类,支持在父子线程间传递上下文。

复制代码
package cn.com.context;

import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 登录上下文对象
 */
public class LoginContextHolder {

    private static final InheritableThreadLocal<Map<String, Object>> THREAD_LOCAL
            = new InheritableThreadLocal<>();

    public static void set(String key, Object val) {
        Map<String, Object> map = getThreadLocalMap();
        map.put(key, val);
    }

    public static Object get(String key){
        Map<String, Object> threadLocalMap = getThreadLocalMap();
        return threadLocalMap.get(key);
    }

    public static String getLoginId(){
        return (String) getThreadLocalMap().get("loginId");
    }

    public static void remove(){
        THREAD_LOCAL.remove();
    }

    public static Map<String, Object> getThreadLocalMap() {
        Map<String, Object> map = THREAD_LOCAL.get();
        if (Objects.isNull(map)) {
            map = new ConcurrentHashMap<>();
            THREAD_LOCAL.set(map);
        }
        return map;
    }


}

四、使用示例

在业务代码中,可以直接通过工具类获取用户信息,无需重复解析header。

复制代码
@RestController
@RequestMapping("/order")
public class OrderController {
    
    @GetMapping("/list")
    public ResponseEntity<List<Order>> listOrders() {
        String loginId = LoginContextHolder.getLoginId();
        log.info("当前登录用户: {}", loginId);
        // 根据loginId查询订单
        return ResponseEntity.ok(orderService.findByUserId(loginId));
    }
}

测试如下:

可以发现请求头传递到下游微服务和基于用户上下文工具类获取用户id都成功了!!!

五、注意事项

  1. 线程池场景 :如果业务中使用了线程池,InheritableThreadLocal可能无法传递,需配合TransmittableThreadLocal或手动传递上下文。

  2. Feign调用 :在服务间通过Feign调用时,需实现RequestInterceptor将上下文中的用户信息注入到Feign请求头中。

  3. 异步处理:在异步线程中需手动传递上下文,或在异步开始时重新设置。

  4. 安全性:网关层应对token进行有效性和权限校验,避免伪造loginId。

六、总结

通过Gateway全局过滤器 + 微服务拦截器 + ThreadLocal上下文持有器的组合,我们实现了:

  • ✅ 网关统一鉴权与用户信息传递

  • ✅ 微服务内无侵入获取用户信息

  • ✅ 线程安全的上下文管理

  • ✅ 链路结束时自动清理,避免内存泄漏

该方案结构清晰、耦合度低,适合在中小型微服务项目中快速落地用户上下文传递机制。

七、ThreadLocal扩展(新开篇章)

一、ThreadLocal:线程隔离的基石

1.1 核心概念与原理

ThreadLocal是Java中最基础的线程本地变量实现。它的核心思想是:每个线程都有自己独立的变量副本,线程之间互不干扰。

复制代码
public class ThreadLocalDemo {
    private static final ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 0);
    
    public static void main(String[] args) {
        // 创建多个线程
        for (int i = 0; i < 3; i++) {
            final int taskId = i;
            new Thread(() -> {
                // 每个线程设置自己的值
                threadLocalValue.set(taskId * 10);
                
                // 模拟业务处理
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                // 每个线程获取自己的值,互不影响
                System.out.println(Thread.currentThread().getName() 
                    + ": " + threadLocalValue.get());
                
                // 重要:使用完毕后清理,避免内存泄漏
                threadLocalValue.remove();
            }, "Thread-" + i).start();
        }
    }
}

1.2 内部实现机制

ThreadLocal的实现非常巧妙,它并不在ThreadLocal对象本身存储数据,而是作为访问线程本地数据的"钥匙":

复制代码
// 简化版实现原理
public class ThreadLocal<T> {
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t); // 获取线程的ThreadLocalMap
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                return (T)e.value;
            }
        }
        return setInitialValue();
    }
    
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }
}

1.3 内存泄漏问题与解决方案

ThreadLocal使用不当会导致内存泄漏,原因在于ThreadLocalMap中的Entry是弱引用:

复制代码
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    
    Entry(ThreadLocal<?> k, Object v) {
        super(k);  // 弱引用指向ThreadLocal
        value = v; // 强引用指向值
    }
}

内存泄漏场景

  1. 线程池中的线程会长期存活

  2. ThreadLocal被回收后,key变为null,但value仍然被Entry强引用

  3. 线程不终止,value就无法被回收

解决方案

复制代码
public class SafeThreadLocalUsage {
    private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();
    
    public void process() {
        try {
            // 使用ThreadLocal
            Connection conn = getConnection();
            connectionHolder.set(conn);
            
            // 业务逻辑处理
            doBusiness();
        } finally {
            // 必须清理!
            connectionHolder.remove();
        }
    }
}

二、InheritableThreadLocal:父子线程传值

2.1 解决的问题场景

在某些场景下,我们需要在创建子线程时,将父线程的线程本地变量传递给子线程。例如,在Web应用中,需要将traceId从请求处理线程传递给异步任务线程。

复制代码
public class InheritableThreadLocalDemo {
    private static final InheritableThreadLocal<String> traceIdHolder = 
        new InheritableThreadLocal<>();
    
    public static void main(String[] args) {
        // 主线程设置traceId
        traceIdHolder.set("TRACE-12345");
        System.out.println("Main thread traceId: " + traceIdHolder.get());
        
        // 创建子线程
        Thread childThread = new Thread(() -> {
            // 子线程可以获取到父线程的traceId
            System.out.println("Child thread traceId: " + traceIdHolder.get());
            
            // 子线程修改不会影响父线程
            traceIdHolder.set("TRACE-54321");
            System.out.println("Child thread modified traceId: " + traceIdHolder.get());
        });
        
        childThread.start();
        
        try {
            childThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // 父线程的值保持不变
        System.out.println("Main thread traceId after child modification: " 
            + traceIdHolder.get());
    }
}

2.2 实现原理

InheritableThreadLocal继承自ThreadLocal,重写了childValuegetMap方法:

复制代码
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    
    // 子线程初始化时调用,决定如何从父线程复制值
    protected T childValue(T parentValue) {
        return parentValue;
    }
    
    // 获取线程的inheritableThreadLocals而不是threadLocals
    ThreadLocalMap getMap(Thread t) {
        return t.inheritableThreadLocals;
    }
}

Thread类的构造函数中,会检查父线程的inheritableThreadLocals并复制:

复制代码
// Thread构造方法中的相关逻辑
if (parent.inheritableThreadLocals != null) {
    this.inheritableThreadLocals = 
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}

2.3 使用注意事项

  1. 线程池中的问题:线程池中的线程是复用的,第一次创建时复制了父线程值后,后续任务会看到之前任务设置的值

  2. 深度复制需求:如果传递的是可变对象,需要考虑是否需要深度复制

三、TransmitThreadLocal:异步编程的救星

3.1 背景与需求

在异步编程和反应式编程中,线程切换非常频繁。InheritableThreadLocal只能在线程创建时传递值,无法在线程池复用场景下正常工作。TransmitThreadLocal(来自阿里巴巴的transmittable-thread-local库)应运而生。

3.2 核心功能

TransmitThreadLocal可以在以下场景中正确传递线程本地变量:

  • 线程池任务提交

  • ForkJoinPool任务提交

  • TimerTask执行

  • 等各种异步执行场景

3.3 基本用法

首先添加依赖:

复制代码
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.14.2</version>
</dependency>

使用示例:

复制代码
public class TransmitThreadLocalDemo {
    // 使用TransmittableThreadLocal
    private static final TransmittableThreadLocal<String> context = 
        new TransmittableThreadLocal<>();
    
    public static void main(String[] args) {
        // 设置初始值
        context.set("main-context");
        
        // 使用线程池
        ExecutorService executor = Executors.newFixedThreadPool(2);
        
        // 普通提交任务(会丢失上下文)
        executor.submit(() -> {
            System.out.println("Normal task: " + context.get()); // null
        });
        
        // 使用TtlRunnable包装任务
        Runnable task = () -> {
            System.out.println("TtlRunnable: " + context.get()); // main-context
        };
        
        // 包装Runnable
        Runnable ttlTask = TtlRunnable.get(task);
        executor.submit(ttlTask);
        
        // 使用TtlExecutors包装线程池(推荐方式)
        ExecutorService ttlExecutor = TtlExecutors.getTtlExecutorService(executor);
        ttlExecutor.submit(() -> {
            System.out.println("TtlExecutor task: " + context.get()); // main-context
        });
        
        executor.shutdown();
    }
}

3.4 实现原理

TransmittableThreadLocal的核心机制是通过TtlRunnable/TtlCallable包装任务,在任务执行前备份当前线程的上下文,在任务执行时恢复,执行后清理:

复制代码
// 简化原理示意
public class TtlRunnable implements Runnable {
    private final Runnable runnable;
    private final Object captured; // 捕获的任务提交时的上下文
    
    public void run() {
        Object backup = replay(); // 恢复捕获的上下文
        try {
            runnable.run();
        } finally {
            restore(backup); // 恢复之前的上下文
        }
    }
}

四、三种方案的对比与选择

4.1 特性对比

特性 ThreadLocal InheritableThreadLocal TransmittableThreadLocal
数据隔离 ✅ 线程级别 ✅ 线程级别 ✅ 线程级别
父子继承 ❌ 不支持 ✅ 支持(仅创建时) ✅ 支持(包括线程池)
线程池支持 ❌ 不支持 ❌ 有限支持 ✅ 完整支持
异步编程 ❌ 不适合 ❌ 不适合 ✅ 非常适合
内存管理 需要手动remove 需要手动remove 需要手动remove
性能开销 较低 中等(需要捕获/恢复)
使用复杂度 简单 中等 较高

4.2 选择指南

  1. 选择ThreadLocal当

    • 只需要线程隔离,不需要跨线程传递

    • 简单的线程局部缓存或上下文存储

    • 性能要求极高,不能接受额外开销

  2. 选择InheritableThreadLocal当

    • 需要从父线程向子线程传递上下文

    • 子线程是新建的,不是来自线程池

    • 简单的异步任务(如new Thread())

  3. 选择TransmittableThreadLocal当

    • 在线程池环境下需要传递上下文

    • 复杂的异步调用链

    • 使用CompletableFuture、反应式编程等现代异步模式

    • 微服务中的调用链跟踪(TraceId传递)

4.3 实战示例:分布式跟踪系统中的上下文传递

复制代码
public class TraceContext {
    // 使用TransmittableThreadLocal存储跟踪上下文
    private static final TransmittableThreadLocal<TraceContext> HOLDER = 
        new TransmittableThreadLocal<>();
    
    private final String traceId;
    private final String spanId;
    private final Map<String, String> baggage;
    
    public static TraceContext getCurrent() {
        return HOLDER.get();
    }
    
    public static void setCurrent(TraceContext context) {
        HOLDER.set(context);
    }
    
    public static void clear() {
        HOLDER.remove();
    }
    
    // 包装线程池
    public static ExecutorService wrapExecutor(ExecutorService executor) {
        return TtlExecutors.getTtlExecutorService(executor);
    }
    
    // 异步执行任务
    public static CompletableFuture<Void> asyncExecute(Runnable task) {
        // 捕获当前上下文
        TraceContext captured = getCurrent();
        
        return CompletableFuture.runAsync(() -> {
            // 恢复上下文
            setCurrent(captured);
            try {
                task.run();
            } finally {
                clear();
            }
        }, wrapExecutor(ForkJoinPool.commonPool()));
    }
}

五、最佳实践与注意事项

5.1 通用最佳实践

  1. 始终清理:使用try-finally确保remove()被调用

  2. 避免存储大对象:ThreadLocal中的对象会长时间存在于内存中

  3. 使用withInitial:提供初始值,避免NullPointerException

  4. 考虑使用包装器:对于复杂对象,考虑使用不可变包装

5.2 针对TransmittableThreadLocal的特殊建议

  1. 统一包装线程池:在应用启动时统一包装所有线程池

  2. 注意对象序列化:如果上下文需要跨JVM传递,确保可序列化

  3. 性能监控:在关键路径监控上下文传递的开销

  4. 兼容性处理:与现有框架(如Spring、Dubbo)集成时注意兼容性

5.3 常见陷阱

复制代码
// 陷阱1:线程池中的内存泄漏
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> {
    ThreadLocal<User> userHolder = new ThreadLocal<>();
    userHolder.set(new User()); // 这个User对象会一直存在直到线程销毁
    // 忘记调用 userHolder.remove()
});

// 陷阱2:InheritableThreadLocal在线程池中的错误使用
executor.submit(task1); // task1设置了上下文
executor.submit(task2); // task2会看到task1设置的上下文!

// 陷阱3:TransmittableThreadLocal包装遗漏
// 错误:直接提交任务
executor.submit(ttlTask); 
// 正确:使用包装后的执行器或任务
ttlExecutor.submit(task);
// 或
executor.submit(TtlRunnable.get(task));

六、结语

线程本地变量是多线程编程中的强大工具,正确使用可以简化代码、提高性能。ThreadLocal提供了基础的线程隔离能力,InheritableThreadLocal扩展了父子线程传值功能,而TransmitThreadLocal解决了异步编程中的上下文传递难题。

在实际开发中,应根据具体场景选择合适的实现。对于现代微服务架构和异步编程模式,TransmitThreadLocal已成为不可或缺的基础组件。无论选择哪种方案,都要牢记内存管理的重要性,避免因使用不当导致的内存泄漏问题。

掌握这些工具,你将能更从容地应对复杂的多线程编程挑战,构建出更健壮、更高效的应用系统。

相关推荐
2601_949194261 天前
Gateway Timeout504 网关超时的完美解决方法
gateway
码点滴2 天前
私有 Gateway 接入企业 IM:从消息路由到多租户隔离——Hermes Agent 工程实战
人工智能·架构·gateway·prompt·智能体·hermes
代码写到35岁2 天前
Gateway+OpenFeign 踩坑总结
gateway
invicinble2 天前
对于gateway信息量沉淀
gateway
郝开3 天前
Spring Cloud Gateway 3.5.14 使用手册
java·数据库·spring boot·gateway
Ribou4 天前
Kubernetes v1.35.2 基于 Cilium Gateway API 的服务访问架构
架构·kubernetes·gateway
huipeng9265 天前
GateWay使用详解
java·spring boot·spring cloud·微服务·gateway
随风,奔跑9 天前
Spring Cloud Alibaba(四)---Spring Cloud Gateway
后端·spring·gateway
jiayong239 天前
Hermes Agent 的 Skills、Plugins、Gateway 深度解析
ai·gateway·agent·hermes agent·hermes
鬼蛟9 天前
Gateway
gateway