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网关验证的完整链路:
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时,必须执行以下检查,缺一不可:
- 签名验证:使用公钥验证Signature,确保令牌未被篡改。
- 过期时间(exp):当前时间必须小于exp,拒绝过期令牌。
- 签发者(iss):必须与SaaS平台预期值一致,防止跨域伪造。
- 租户一致性:Payload中的tenantId必须与请求路径或域名中的租户标识匹配。
- 角色有效性:role必须在当前租户的预定义角色列表中,防止越权。
- 令牌撤销检查:查询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的组合方案,在安全性与性能之间取得平衡。