4、租户身份标识:租户ID的设计、JWT中的租户声明、请求上下文中的租户传递

在多租户控制面架构中,租户身份标识是权限隔离的基石。每个请求必须携带明确的租户身份,系统才能判断该请求应访问哪些资源、执行哪些操作。本章围绕租户ID的设计原则、JWT中的租户声明结构以及请求上下文的传递机制展开,确保身份标识在微服务链路中不丢失、不被篡改。

4.1 租户ID的设计原则

租户ID(Tenant ID)是区分不同租户的唯一标识符。设计不当的租户ID会导致路由混乱、权限越界或性能瓶颈。以下是核心设计原则:

原则 说明 推荐做法
全局唯一 租户ID必须在整个控制面集群内唯一,不能跨租户重复 使用UUID v4或Snowflake算法生成
不可猜测 避免使用自增数字或可枚举的短字符串 采用随机字符串或哈希值,长度≥16字符
不可篡改 租户ID一旦分配,不应允许租户自行修改 由控制面统一分配,存储在租户注册表中
可路由 支持按租户ID进行数据分片或服务路由 设计时预留分区键位,如Snowflake中的工作节点ID
可读性 在日志和审计中能快速识别租户归属 可选前缀+随机串,如 tnt_a3f8c2b1
实践建议: 推荐使用 UUID v7(时间有序UUID),既满足全局唯一性,又支持按时间排序,便于数据库索引和日志检索。避免使用纯数字自增ID,防止租户枚举攻击。

4.2 JWT中的租户声明

JWT(JSON Web )是微服务间传递身份信息的标准载体。租户ID应作为JWT的私有声明(Private Claim)嵌入,而非放在公共声明中。标准结构如下:

{
  "iss": "https://auth.controlplane.io",
  "sub": "user_abc123",
  "aud": "api-gateway",
  "exp": 1718000000,
  "iat": 1717996400,
  "tenant_id": "tnt_a3f8c2b1",        // 租户ID
  "tenant_role": "admin",             // 租户内角色
  "tenant_features": ["audit", "backup"] // 租户特性标记
}

JWT中租户声明的关键设计要点:

  • 声明名称标准化: 统一使用 tenant_id 作为键名,避免各服务自行定义导致解析混乱。
  • 签名保护: 租户声明必须包含在JWT的签名部分,防止中间人篡改。使用RS256或ES256非对称算法,确保服务间验证时不暴露私钥。
  • 最小化声明: 只放入路由和权限判断必需的字段,避免携带大量租户元数据导致膨胀。元数据可通过租户ID查询缓存获取。
  • 过期与刷新: 租户声明随JWT一起过期,建议有效期不超过15分钟,配合Refresh 实现长会话。
安全警告: 不要在JWT的 sub 或 iss 等公共声明中嵌入租户ID。公共声明有标准语义,混用会导致兼容性问题,且容易被解析库忽略签名验证。始终使用私有声明区域。

4.3 请求上下文中的租户传递

在微服务调用链中,JWT通常在网关层验证并解析,但后续服务间调用不可能每次都重新解析JWT。因此需要将租户ID从请求上下文(Context)中透传。以下是三种主流传递方式:

传递方式 适用场景 优点 缺点
gRPC Metadata 服务间gRPC调用 原生支持、低延迟、自动传播 需手动注入拦截器
HTTP Header REST API调用 通用性强、调试方便 需防止客户端伪造
消息队列Header 异步事件驱动 解耦、支持批量 需保证消息完整性

在gRPC场景中,推荐使用统一的客户端拦截器/中间件,从JWT中提取 tenant_id 并注入到 metadata 中:

// Go示例:gRPC客户端拦截器注入租户ID
func TenantUnaryClientInterceptor() grpc.UnaryClientInterceptor {
    return func(ctx context.Context, method string, req, reply interface{},
        cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
        // 从当前context中提取租户ID
        tenantID := ctx.Value("tenant_id").(string)
        // 注入到gRPC metadata
        md := metadata.Pairs("x-tenant-id", tenantID)
        ctx = metadata.NewOutgoingContext(ctx, md)
        return invoker(ctx, method, req, reply, cc, opts...)
    }
}

在HTTP场景中,网关验证JWT后,将租户ID写入 X-Tenant-ID 请求头,下游服务从请求头中读取。注意:网关必须剥离客户端传入的原始 X-Tenant-ID 头,防止租户伪造。

关键流程: 客户端 → 网关(验证JWT,提取tenant_id,覆盖请求头) → 服务A(从请求头读取tenant_id,注入本地Context) → 服务B(通过gRPC Metadata传递) → 数据库(按tenant_id分片查询)。整个链路中租户ID始终存在且受信任。
租户ID在请求链路中的传递流程 客户端 API网关 服务A 服务B JWT(含tenant_id) X-Tenant-ID gRPC Metadata ① 验证JWT签名 ② 提取tenant_id ③ 覆盖请求头 ① 读取请求头 ② 注入本地Context ③ 传递至下游 ① 读取Metadata ② 执行权限检查 ③ 按tenant_id查询 传递方式对比 HTTP Header:X-Tenant-ID gRPC Metadata:x-tenant-id MQ Header:tenant_id

4.4 租户ID的传递安全与防篡改

租户ID在传递过程中面临的主要风险是中间人篡改和客户端伪造。以下是针对性的防护措施:

  • 网关强制覆盖: 所有进入内网的请求,网关必须剥离客户端传入的 X-Tenant-ID 头,重新从JWT中提取并注入。内网服务不信任任何来自客户端的租户ID。
  • mTLS传输加密: 服务间调用启用双向TLS,防止中间人篡改请求头或Metadata。
  • Context传播审计: 在关键服务入口记录租户ID的来源(JWT/Header/Metadata),便于审计链路是否被污染。
  • 租户ID白名单: 对于敏感操作(如删除资源),服务端可二次校验租户ID是否在活跃租户白名单中,防止已删除租户的残留被滥用。
审计最佳实践: 在日志中统一使用 tenant_id 字段,配合 request_id 和 user_id,形成完整的请求链路追踪。推荐使用OpenTelemetry的Baggage机制,将租户ID自动传播到所有Span中,无需手动传递。

4.5 多级租户场景下的ID扩展

在复杂的企业级多租户架构中,可能存在组织(Organization)→ 项目(Project)→ 环境(Environment)的多级租户结构。此时租户ID需要支持层级表达:

层级 示例ID 说明
组织级 org_a3f8c2b1 顶级租户,拥有独立资源池和计费
项目级 proj_d4e5f6a7 属于某个组织,共享部分资源
环境级 env_b8c9d0e1 开发/测试/生产环境隔离

在JWT中,可以使用复合声明或嵌套对象表示层级关系:

{
  "tenant_id": "org_a3f8c2b1",
  "tenant_hierarchy": {
    "org_id": "org_a3f8c2b1",
    "project_id": "proj_d4e5f6a7",
    "env_id": "env_b8c9d0e1"
  },
  "tenant_role": "project_admin"
}

权限判断时,服务端根据操作所需的层级粒度,从 tenant_hierarchy 中提取对应的ID进行校验。例如,创建Pod时校验 env_id,查看账单时校验 org_id。

多级租户ID在JWT中的层级结构 JWT Payload iss / sub / aud / exp / iat(标准声明) tenant_id: "org_a3f8c2b1"(主租户ID) tenant_hierarchy(层级声明) org_id: "org_a3f8c2b1" project_id: "proj_d4e5f6a7" env_id: "env_b8c9d0e1" 权限判断时根据操作粒度选择对应层级ID进行校验

4.6 租户ID的存储与缓存策略

租户ID不仅在请求中传递,还需要在服务端持久化存储和缓存,以支持高效的权限查询。以下是推荐策略:

  • 数据库字段: 所有多租户表必须包含 tenant_id 列,并建立联合索引(tenant_id + 业务主键)。
  • Redis缓存: 以 tenant:{tenant_id}:features 为键,缓存租户特性、配额、角色等元数据,TTL建议5分钟。
  • 本地缓存: 对于高频读取的租户配置(如路由规则),使用Caffeine或Guava Cache做本地缓存,配合Redis发布订阅实现失效通知。
  • 分片策略: 按租户ID哈希分片,确保同一租户的数据落在同一数据库分片,避免跨分片查询。
总结: 租户ID是贯穿整个控制面的核心标识。设计上要保证唯一性、不可篡改性和可路由性;传递上要依赖JWT签名和网关强制覆盖;存储上要建立以tenant_id为第一维度的索引和缓存体系。只有租户ID在每一层都得到正确传递和验证,多租户权限隔离才有坚实的基础。