2. 鉴权基础理论:认证与授权区别、OAuth2.0流程、JWT令牌结构解析
2.1 认证 vs 授权:核心概念辨析
在构建多租户SaaS鉴权体系之前,必须清晰区分两个经常被混淆的概念:认证(Authentication)与授权(Authorization)。它们是安全链路中的两道独立关卡,职责完全不同。
| 维度 | 认证(Authentication) | 授权(Authorization) |
|---|---|---|
| 核心问题 | 你是谁? | 你能做什么? |
| 目的 | 验证用户身份的真实性 | 授予用户访问资源的权限 |
| 执行顺序 | 先执行(第一步) | 后执行(第二步) |
| 传递凭证 | 用户名/密码、生物特征、SSO令牌 | Access Token、权限票据 |
| 失败结果 | 401 Unauthorized(未认证) | 403 Forbidden(无权限) |
| SaaS场景示例 | 租户管理员登录控制台 | 普通用户只能查看本租户数据 |
常见误区:很多开发者在拦截链路中只做认证,忽略授权检查,导致用户登录后可以越权访问其他租户的数据。在多租户SaaS中,认证通过后必须立即执行租户级授权校验。
2.2 OAuth2.0 授权流程详解
OAuth2.0 是当前业界最主流的授权框架,定义了四种授权模式。在多租户SaaS场景中,授权码模式(Authorization Code Grant)是最安全、最常用的模式,尤其适用于有后端的Web应用。
2.2.1 授权码模式完整流程
- 用户发起请求:用户通过浏览器访问客户端应用,点击“第三方登录”。
- 客户端构造授权请求:客户端将用户重定向到授权服务器(Authorization Server),携带
client_id、redirect_uri、response_type=code和scope。 - 用户认证并授权:用户在授权服务器上输入凭证,确认授权范围。
- 授权服务器返回授权码:授权服务器通过重定向将
authorization_code发送到客户端的redirect_uri。 - 客户端换取令牌:客户端后端使用
authorization_code+client_secret向授权服务器的令牌端点(Token Endpoint)发起POST请求。 - 授权服务器返回令牌:返回
access_token(访问令牌)和可选的refresh_token(刷新令牌)。 - 客户端访问资源:客户端携带
access_token请求资源服务器(Resource Server)的受保护API。 - 资源服务器验证令牌:资源服务器验证令牌的有效性,返回资源数据。
多租户关键点:在步骤6返回的
access_token 中,必须包含 tenant_id 和 role 声明,以便资源服务器在后续请求中执行租户隔离和权限校验。
2.2.2 四种授权模式对比
| 授权模式 | 适用场景 | 安全性 | 多租户适用性 |
|---|---|---|---|
| 授权码模式 | 有后端的Web应用 | 最高(令牌不暴露给浏览器) | ★★★★★ 推荐 |
| 隐式模式 | 纯前端SPA(已不推荐) | 低(令牌暴露在URL中) | ★★☆☆☆ 不推荐 |
| 密码模式 | 高度信任的第一方应用 | 中(需存储用户密码) | ★★★☆☆ 谨慎使用 |
| 客户端凭证模式 | 服务间通信(M2M) | 高(无用户参与) | ★★★★☆ 适合微服务 |
2.3 JWT令牌结构解析
JWT(JSON Web Token)是OAuth2.0中最常用的令牌格式。它是一个自包含的、紧凑的、URL安全的令牌,由三部分组成,每部分用点号(.)分隔。
2.3.1 JWT三段式结构
# JWT 令牌示例(已解码)
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyXzEyMyIsInRlbmFudF9pZCI6InRlbmFudF9hYmMiLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMzYwMH0.
s9f3k2j4h5g6f7d8s9a0q1w2e3r4t5y6u7i8o9p0
# 解码后的三部分
# Header(头部)
{
"alg": "RS256",
"typ": "JWT"
}
# Payload(载荷)
{
"sub": "user_123",
"tenant_id": "tenant_abc",
"role": "admin",
"iat": 1700000000,
"exp": 1700003600
}
# Signature(签名)
# 使用私钥对 header + "." + payload 进行签名
2.3.2 三部分详细说明
| 部分 | 名称 | 内容 | 是否加密 | 作用 |
|---|---|---|---|---|
| Header | 头部 | 令牌类型(typ)和签名算法(alg) | Base64编码(非加密) | 声明令牌的元数据 |
| Payload | 载荷 | 声明(Claims),包含用户身份、租户ID、角色、过期时间等 | Base64编码(非加密) | 携带业务数据,切勿存放敏感信息 |
| Signature | 签名 | 对Header和Payload的签名结果 | 加密(使用私钥) | 验证令牌完整性,防止篡改 |
安全警告:JWT的Payload只是Base64编码,不是加密!任何拿到令牌的人都可以解码查看Payload内容。绝对不要在Payload中存放密码、信用卡号、个人隐私等敏感数据。如果需要保护Payload内容,请使用JWE(JSON Web Encryption)。
2.3.3 多租户SaaS中的JWT声明设计
在多租户架构中,JWT的Payload需要包含租户上下文信息,以便资源服务器进行租户隔离和权限校验。以下是推荐的声明设计:
{
// 标准注册声明(Registered Claims)
"iss": "https://auth.you-saas.com", // 签发者
"sub": "user_8a3f7b2c1d", // 用户唯一标识
"aud": "https://api.you-saas.com", // 接收方
"exp": 1700003600, // 过期时间(必填)
"iat": 1700000000, // 签发时间
"jti": "token_9k2m4n6p8q", // 令牌唯一ID(防重放)
// 私有声明(Private Claims)- 多租户关键字段
"tenant_id": "tenant_abc", // 租户ID(必填)
"role": "admin", // 角色(必填)
"permissions": [ // 权限列表(可选)
"user:read",
"user:write",
"report:export"
],
"tenant_features": [ // 租户功能特性(可选)
"advanced_analytics",
"api_access"
]
}
2.3.4 JWT签名算法选择
| 算法 | 类型 | 密钥 | 性能 | 推荐场景 |
|---|---|---|---|---|
| HS256 | 对称 | 单一共享密钥 | 快 | 单体应用、内部服务 |
| RS256 | 非对称 | 私钥签名 / 公钥验证 | 较慢 | 多租户SaaS推荐、微服务架构 |
| ES256 | 非对称(椭圆曲线) | 私钥签名 / 公钥验证 | 快(密钥更短) | 移动端、IoT设备 |
最佳实践:在多租户SaaS中,推荐使用RS256算法。授权服务器持有私钥进行签名,各个微服务(资源服务器)只需持有公钥即可验证令牌,无需共享密钥,大大降低了密钥泄露的风险。
2.4 认证与授权在拦截链路中的位置
在SaaS鉴权拦截链路中,认证和授权是两个独立的拦截器(Middleware),执行顺序不可颠倒:
# Python Flask 示例:认证与授权拦截器链
from functools import wraps
from flask import request, jsonify, g
# 第一道关卡:认证拦截器
def authenticate_middleware(f):
@wraps(f)
def decorated(*args, **kwargs):
token = request.headers.get('Authorization')
if not token:
return jsonify({"error": "Missing token"}), 401
try:
# 验证JWT签名和过期时间
payload = verify_jwt(token)
g.current_user = payload['sub']
g.tenant_id = payload['tenant_id']
g.role = payload['role']
except Exception as e:
return jsonify({"error": "Invalid token"}), 401
return f(*args, **kwargs)
return decorated
# 第二道关卡:授权拦截器
def authorize_middleware(required_permission):
def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
# 从JWT中获取权限列表
permissions = g.get('permissions', [])
# 检查是否拥有所需权限
if required_permission not in permissions:
return jsonify({"error": "Insufficient permissions"}), 403
return f(*args, **kwargs)
return decorated
return decorator
# 使用示例:先认证,后授权
@app.route('/api/tenant/users')
@authenticate_middleware
@authorize_middleware('user:read')
def get_tenant_users():
# 此时 g.tenant_id 已可用,自动隔离租户数据
users = query_users_by_tenant(g.tenant_id)
return jsonify(users)
拦截链路核心原则:认证在前,授权在后。认证失败返回401,授权失败返回403。在多租户SaaS中,授权拦截器必须同时校验角色和租户ID,确保用户只能访问本租户的资源。