4. JWT与租户令牌设计

在多租户SaaS架构中,JWT(JSON Web Token)是承载租户身份与权限的核心令牌载体。本章聚焦JWT的结构拆解、自定义Claims设计以及令牌签发与验证的完整流程,确保每个请求都能准确识别租户上下文。

4.1 JWT标准结构

JWT由三个Base64URL编码的部分组成,以点号(.)分隔:Header.Payload.Signature。下表列出各部分的构成与职责:

部分 字段 说明 示例值
Header alg 签名算法,常用RS256或HS256 "RS256"
typ 令牌类型,固定为JWT "JWT"
Payload sub 用户唯一标识(用户ID) "user_abc123"
iss 签发者(SaaS平台域名) "saas.example.com"
iat 签发时间(Unix时间戳) 1718000000
exp 过期时间(Unix时间戳) 1718086400
tenantId 自定义Claim:租户ID "tenant_xyz"
role 自定义Claim:用户角色 "admin"
Signature — 对Header+Payload的签名,防篡改 base64url(RSASHA256(...))
关键设计原则: 自定义Claims(tenantId、role)必须放在Payload的“私有声明”区域,避免与标准注册声明冲突。签名算法推荐使用非对称RS256,便于微服务间公钥验证,无需共享密钥。

4.2 自定义Claims设计:tenantId与role

在多租户场景中,tenantId 和 role 是鉴权拦截链路的核心字段。设计时需遵循以下规范:

  • tenantId:字符串类型,全局唯一,用于标识请求所属的租户。签发时从用户认证上下文获取,不可由客户端自行声明。
  • role:字符串或字符串数组,定义用户在租户内的权限级别。常见值:admin、editor、viewer。
  • 作用域:role 必须与 tenantId 绑定,避免跨租户角色滥用。例如:同一用户在租户A是admin,在租户B可能只是viewer。
  • 最小化原则:Payload中只放鉴权必需字段,不存放用户姓名、邮箱等敏感个人信息,减少令牌泄露风险。
最佳实践: 在Payload中额外加入 jti(JWT ID)字段,用于令牌撤销场景。当用户退出或租户配置变更时,可通过jti将指定令牌加入黑名单。

4.3 令牌签发与验证流程

下图展示从用户登录到令牌签发,再到API网关验证的完整链路:

JWT签发与验证流程 JWT签发与验证流程 用户登录 认证服务 签发JWT 提交凭证 验证通过 客户端存储 返回JWT API请求 携带JWT(Authorization头) API网关验证 验证通过 签名+Claims校验 转发到后端服务 携带tenantId/role 说明:API网关验证JWT签名、exp过期时间、tenantId与请求路径的租户一致性,验证通过后将Claims注入请求上下文。

4.4 签发与验证代码示例

以下示例使用Node.js + jsonwebtoken库演示JWT签发与验证的核心逻辑:

// 签发JWT(认证服务端)
const jwt = require('jsonwebtoken');
const privateKey = fs.readFileSync('private.pem');

function issueToken(user) {
  const payload = {
    sub: user.id,
    iss: 'saas.example.com',
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + 3600, // 1小时过期
    tenantId: user.tenantId,
    role: user.role,
    jti: uuid.v4() // 令牌唯一ID,用于撤销
  };
  return jwt.sign(payload, privateKey, { algorithm: 'RS256' });
}

// 验证JWT(API网关端)
const publicKey = fs.readFileSync('public.pem');

function verifyToken(token) {
  try {
    const decoded = jwt.verify(token, publicKey, { 
      algorithms: ['RS256'],
      issuer: 'saas.example.com'
    });
    // 验证通过,返回payload
    return { valid: true, payload: decoded };
  } catch (err) {
    // 签名无效、过期、签发者不匹配等
    return { valid: false, error: err.message };
  }
}
安全警告: 私钥必须妥善保管,禁止硬编码在代码仓库中。生产环境建议使用密钥管理服务(KMS)或Vault存储私钥。公钥可通过/.well-known/jwks.json端点暴露给各微服务。

4.5 令牌验证关键检查项

API网关在验证JWT时,必须执行以下检查,缺一不可:

  1. 签名验证:使用公钥验证Signature,确保令牌未被篡改。
  2. 过期时间(exp):当前时间必须小于exp,拒绝过期令牌。
  3. 签发者(iss):必须与SaaS平台预期值一致,防止跨域伪造。
  4. 租户一致性:Payload中的tenantId必须与请求路径或域名中的租户标识匹配。
  5. 角色有效性:role必须在当前租户的预定义角色列表中,防止越权。
  6. 令牌撤销检查:查询jti是否在黑名单中(可选,用于登出/踢人场景)。
性能优化: 令牌验证是高频操作,建议在API网关层使用缓存(如Redis)存储公钥和撤销列表,避免每次验证都查询数据库。JWT本身无状态,验证只需公钥和内存计算,性能远优于Session方案。

4.6 租户令牌设计对比

下表对比不同令牌设计方案的适用场景:

方案 优点 缺点 推荐场景
单JWT(含tenantId) 结构简单,无状态,验证快 令牌撤销需黑名单,Payload体积大 中小型SaaS,租户数<1000
JWT + 引用令牌 支持即时撤销,Payload小 需中心化存储,增加网络开销 大型SaaS,高安全要求
双层JWT(Access + Refresh) 短时效Access Token降低泄露风险,Refresh Token持久化 实现复杂度高,需管理Refresh Token 移动端/单页应用,用户体验优先

在多租户SaaS鉴权拦截链路中,推荐采用单JWT + 短过期时间(15-30分钟)+ Refresh Token的组合方案,在安全性与性能之间取得平衡。