4. 鉴权基础概念:认证与授权区别、Token 与 Session、JWT 结构解析
在多租户 SaaS 架构中,鉴权(Authentication & Authorization)是拦截链路的第一道防线。很多开发者混淆了“认证”与“授权”的职责,导致租户隔离出现严重漏洞。本节从最基础的概念出发,厘清认证与授权的本质区别,对比 Token 与 Session 的适用场景,并深入解析 JWT 的结构与安全约束。
4.1 认证 vs 授权:两个截然不同的安全职责
认证(Authentication)回答“你是谁”,授权(Authorization)回答“你能做什么”。在 SaaS 系统中,认证通常由身份提供者(IdP)完成,授权则由策略决策点(PDP)执行。两者必须分离,否则无法实现细粒度的租户权限控制。
| 维度 | 认证(Authentication) | 授权(Authorization) |
|---|---|---|
| 核心问题 | 你是谁? | 你能做什么? |
| 输入 | 凭证(密码、MFA、SSO Token) | 身份信息 + 资源标识 + 操作意图 |
| 输出 | 身份声明(Subject / Principal) | 允许 / 拒绝决策 |
| 存储 | 用户表、LDAP、OIDC Provider | 策略表、RBAC 角色、ABAC 属性 |
| 典型协议 | OAuth2 授权码流程、SAML2、LDAP Bind | OAuth2 Scope、XACML、Casbin |
| 多租户关注点 | 租户身份源隔离、密码哈希算法 | 租户级资源权限、跨租户越权防护 |
实战提示: 在 SaaS 拦截链路中,认证失败应返回
401 Unauthorized,授权失败应返回 403 Forbidden。很多团队错误地将所有鉴权失败统一返回 401,导致客户端无法区分“未登录”与“无权限”,增加调试成本。
4.2 Token 与 Session:两种会话管理范式的对比
Session 和 Token 是两种主流的会话状态管理方式。Session 是“服务端有状态”模型,Token 是“客户端自包含”模型。在分布式多租户 SaaS 中,Token 方案更具扩展性,但需要更严格的安全处理。
| 特性 | Session | Token(JWT) |
|---|---|---|
| 状态存储位置 | 服务端内存 / Redis | 客户端(Token 自身携带) |
| 扩展性 | 需共享 Session 存储(粘性 Session 或集中缓存) | 无状态,任意节点均可验证 |
| 跨域支持 | 受限于 Cookie SameSite 策略 | 可通过 Authorization Header 自由传递 |
| 吊销能力 | 直接删除服务端 Session 即可 | 需维护黑名单或缩短有效期 |
| 多租户隔离 | Session 中存储 tenant_id,需防篡改 | JWT 中嵌入 tenant_id,依赖签名防篡改 |
| 典型适用场景 | 单体应用、内网系统 | 微服务、移动端、跨域 SaaS |
安全警告: 使用 Session 时,务必设置
HttpOnly、Secure、SameSite=Lax/Strict 属性,防止 XSS 窃取 Session ID。使用 Token 时,禁止将 Token 明文存储在 localStorage 中,优先使用 httpOnly Cookie 或 Authorization Header + 短有效期 方案。
4.3 JWT 结构解析:Header、Payload、Signature
JWT(JSON Web Token)是目前 SaaS 系统中最常用的自包含 Token 格式。一个完整的 JWT 由三部分组成,以点号(.)分隔:Header.Payload.Signature。下面通过一个实际解码后的 JWT 示例来拆解每一部分。
// 原始 JWT(Base64URL 编码)
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyXzEyMyIsInRlbmFudF9pZCI6InRlbmFudF9hYmMiLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMzYwMH0.
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQ...
// 解码后的 Header
{
"alg": "RS256",
"typ": "JWT"
}
// 解码后的 Payload
{
"sub": "user_123",
"tenant_id": "tenant_abc",
"role": "admin",
"iat": 1700000000,
"exp": 1700003600
}
4.3.1 Header(头部)
- alg:签名算法,常见值
HS256(对称)、RS256(非对称)、ES256。 - typ:类型,固定为
JWT。 - kid(可选):密钥 ID,用于多密钥轮换场景。
4.3.2 Payload(载荷)
- Registered Claims:预定义声明,如
iss(签发者)、sub(主题)、aud(受众)、exp(过期时间)、iat(签发时间)。 - Public Claims:公开自定义声明,需在 IANA 注册或使用命名空间避免冲突。
- Private Claims:私有自定义声明,如
tenant_id、role、permissions。
4.3.3 Signature(签名)
签名由 Header 中指定的算法对 Header + "." + Payload 进行加密生成。使用对称算法(HS256)时,服务端和客户端共享同一个密钥;使用非对称算法(RS256)时,服务端持有私钥签名,客户端或第三方服务使用公钥验证。
多租户关键点: 务必在 JWT Payload 中显式包含
tenant_id 声明,并在验证环节强制校验该字段与请求路径/域名中的租户标识一致,防止租户 A 的 Token 被用于访问租户 B 的资源。
4.4 多租户场景下的 JWT 安全约束
在多租户 SaaS 中,JWT 的使用必须遵循以下安全约束,否则极易出现租户越权漏洞:
- 强制校验 tenant_id:在 API 网关或拦截器中,将 JWT 中的
tenant_id与请求路径(如/api/{tenant_id}/resource)或请求头中的租户标识进行比对,不一致则直接拒绝。 - 使用非对称算法(RS256/ES256):避免使用对称算法(HS256),因为对称密钥一旦泄露,攻击者可伪造任意租户的 Token。非对称算法下,服务端私钥不对外暴露,客户端仅持有公钥。
- 短有效期 + 刷新机制:Access Token 有效期建议不超过 15 分钟,配合 Refresh Token 实现无感续期。长有效期 Token 会增加泄露后的风险窗口。
- 禁止在 Payload 中存储敏感数据:Payload 仅做 Base64URL 编码,并非加密。密码、信用卡号等敏感信息不得放入 JWT。
- 黑名单机制:对于需要立即吊销的场景(如用户退出、租户禁用),维护一个 Redis 黑名单,每次验证时检查 JWT 的
jti(JWT ID)是否在黑名单中。
实战案例: 某 SaaS 平台曾因 JWT 中未校验
tenant_id,导致租户 A 的用户通过修改请求路径中的租户 ID,成功访问租户 B 的客户数据。修复方案:在 API 网关层增加 tenant_id 一致性校验,并将该校验逻辑前置到认证过滤器之前。
4.5 常见鉴权漏洞与防御清单
| 漏洞类型 | 攻击方式 | 防御措施 |
|---|---|---|
| JWT 未校验签名 | 攻击者修改 Payload 中的 tenant_id,服务端未验证签名即信任 | 强制校验 JWT Signature,使用非对称算法 |
| Session 固定攻击 | 攻击者诱导用户使用已知 Session ID | 登录后重新生成 Session ID,设置 Secure 和 HttpOnly |
| Token 泄露 | Token 被 XSS 窃取或 HTTPS 中间人截获 | 使用 httpOnly Cookie 存储 Token,启用 HSTS,短有效期 |
| 跨租户越权 | 使用租户 A 的 Token 访问租户 B 的资源 | 在拦截链路中强制校验 tenant_id 一致性 |
| 算法混淆攻击 | 将 RS256 改为 HS256,使用公钥作为对称密钥验证 | 服务端固定允许的算法列表,拒绝非预期算法 |
推荐实践: 在 SaaS 项目中,建议使用 OAuth2 + JWT(RS256) 作为认证授权基础方案。OAuth2 负责授权码流程和 Token 颁发,JWT 负责自包含的身份与租户声明。API 网关层统一完成 JWT 验证和 tenant_id 校验,后端服务无需重复实现鉴权逻辑。