第4章 租户上下文传递设计:请求链路中的租户ID传递机制、线程局部变量与JWT Claims

边界定义

租户上下文传递的边界起点为用户请求到达系统入口(API网关或负载均衡器),终点为数据访问层执行租户隔离过滤。整个传递链路跨越网关层、认证层、业务服务层与数据访问层,不包含后台批处理或离线任务(此类场景通过独立的租户上下文初始化器处理)。上下文载体包括:HTTP请求头、JWT Claims、线程局部变量(ThreadLocal)以及RPC元数据。

分层架构与组件职责

分层组件职责
网关层Token解析过滤器从Authorization头提取JWT,校验签名与过期时间,将JWT Claims中的tenant_id写入HTTP请求头X-Tenant-ID
认证层身份校验中间件验证JWT完整有效性,解析出租户ID后生成认证上下文对象,包含用户ID、角色列表、租户ID
业务服务层租户上下文拦截器取请求头或认证上下文中的租户ID,存入ThreadLocal变量;在请求结束时清除防止内存泄漏
数据访问层多租户过滤器从ThreadLocal读取租户ID,注入SQL WHERE子句或ORM拦截器,实现行级数据隔离
关键设计原则:租户上下文的存储与传播必须与请求线程绑定,且不允许跨线程泄漏;任何异步或线程池操作都必须显式传递上下文(使用继承型ThreadLocal或ContextPropagation库)。

数据流与信任链路

完整的租户ID传递数据流如下:

  1. 客户端在请求头Authorization: Bearer <JWT>中携带经过签名的JWT。
  2. API网关校验JWT签名后,将Claims中的tenant_id提取并存入HTTP请求头X-Tenant-ID(同时可选覆盖原JWT,以消除下游对JWT的二次解析依赖)。
  3. 认证服务(或网关自身)完成用户身份与权限校验,生成包含租户ID的认证令牌(可能仍为JWT,但加入内部签名)。
  4. 业务服务的拦截器从X-Tenant-ID读取租户ID,或从认证上下文读取,写入当前线程的ThreadLocal。
  5. 若业务服务需要调用下游服务(微服务场景),则通过RPC元数据(gRPC metadata)或HTTP头将tenant_id透传。
  6. 数据访问层通过MyBatis/Spring Data的拦截器从ThreadLocal获取租户ID,动态拼接WHERE tenant_id = ?。
信任链路说明:从网关到下游服务,每个环节都必须对租户ID的来源进行信任验证。网关是信任锚点——只有网关从合法JWT中提取的租户ID才是可信的;下游服务不应信任请求头直接传入的X-Tenant-ID(防止伪造),而应依赖网关注入的经过签名的内部令牌或请求头与原始JWT的双重校验。

选型权衡:上下文传递机制对比

方案优势劣势适用场景
JWT Claims直接传递无状态、端到端安全;无需额外存储每次请求解析额外开销;JWT体积膨胀单体或无状态微服务
HTTP头X-Tenant-ID简单、易调试;网关可统一注入需要防止外部伪造;需额外校验网关代理模式,内部服务间调用
gRPC Metadata天然支持上下文传播;与protobuf集成仅适用于gRPC协议;跨语言适配复杂微服务全gRPC链路
ThreadLocal透明、无侵入;与ORM无缝集成异步/线程池场景需显式传递;不当使用导致内存泄漏同步请求处理(Web应用核心层)
分布式上下文传播库自动跨线程、跨网络传播;集成追踪(如OpenTelemetry)增加依赖与学习成本;调试复杂度提升大型微服务/事件驱动架构

风险点与应对策略

  • 租户上下文丢失:当请求进入异步线程(如@Async、CompletableFuture)或线程池时,ThreadLocal变量不会自动继承。应对:使用InheritableThreadLocal(注意子线程会在创建时快照,后续父线程变更不更新)或显式传递上下文(如将租户ID作为方法参数传入异步任务)。
  • 内存泄漏:请求结束后未清除ThreadLocal,导致容器线程池复用后残留旧租户信息。强制在过滤器/拦截器的finally块中调用remove()。
  • 租户ID伪造:未校验内部请求头来源,攻击者直接发送X-Tenant-ID绕过权限。策略:网关在注入租户ID前必须删除原始请求头中的该字段;内部服务间使用mTLS或共享密钥签名。
  • 异步消息队列传递:消息生产者将租户ID写入消息头,消费者在消费时从消息头提取并写入ThreadLocal,需保证消息头字段不被篡改(消息签名)。

落地步骤

  1. 定义统一租户上下文接口:设计TenantContext接口,包含getTenantId()、setTenantId()、clear()方法,底层使用ThreadLocal实现。
  2. 实现网关注入过滤器:在网关层(如Spring Cloud Gateway)编写全局过滤器,从JWT中提取tenant_id,写入请求头并删除原有X-Tenant-ID。
  3. 实现服务端拦截器:在业务服务中注册HandlerInterceptor或Java Filter,在preHandle中从请求头或认证对象读取租户ID并存入TenantContext,在afterCompletion中调用clear()。
  4. 配置ORM多租户插件:以MyBatis为例,实现Interceptor接口拦截Executor.query(),从TenantContext获取租户ID并追加SQL条件。
  5. 异步线程适配:对@Async或自定义线程池,包装Runnable或Callable,将父线程的TenantContext拷贝到子线程(使用TransmittableThreadLocal或手动传递)。
  6. 跨服务传递:在RPC客户端拦截器中自动将TenantContext.getTenantId()附加到gRPC Metadata或HTTP头。

验收标准

条目验证方式预期结果
同步请求租户隔离使用不同租户的JWT访问同一接口返回各自租户的数据,不存在数据交叉
异步线程上下文有效在@Async方法中使用TenantContext子线程正确获取与请求一致的租户ID
请求结束后无泄漏持续压力测试,监控线程局部变量ThreadLocal内存不增长,无陈旧租户残留
伪造租户ID被拦截直接向服务发X-Tenant-ID请求头被网关或服务端校验拒绝,返回401/403
跨服务透传成功微服务A调用B,B的上下文包含正确租户IDB的数据访问按租户隔离
最佳实践提醒:在Spring Boot项目中,可直接使用RequestContextHolder配合单独封装的TenantContext,但需注意RequestContextHolder本身依赖ServletRequest,不适合异步场景。推荐使用独立的TransmittableThreadLocal实现,并集成到TaskDecorator中自动传递。
租户上下文传递流程图 租户ID请求链路传递机制 客户端 ① 请求 API网关 ② JWT解析+校验 认证服务 ③ 身份验证 业务服务 ④ 拦截器注入 ThreadLocal ⑤ 存储租户ID 数据访问层 ⑥ SQL注入 租户隔离数据库 ⑦ WHERE tenant_id=? X-Tenant-ID 读取tenant_id 要点: ① 网关是信任锚点,负责从JWT提取租户ID并覆盖请求头;② 内部服务间通过mTLS或共享密钥保证请求头不可伪造;③ 线程局部变量在请求结束后必须清除,异步场景需显式传递。

本章详细阐述了租户ID在请求全链路中的传递机制、组件职责与实现步骤。下一环节将进入《第5章:数据库多租户隔离策略:共享表、独立Schema与混合模型》,讨论底层数据存储层面的隔离设计。