接口鉴权规范
Token、权限、签名等鉴权机制
[Standard] 面向终端用户的接口使用 JWT Bearer Token;服务间调用使用 AppKey + AppSecret 签名;内部管理接口使用 mTLS 或内网白名单。
| 场景 | 认证方式 | 传输位置 |
|---|---|---|
| 终端用户(Web/App) | JWT Access Token | Authorization: Bearer <token> |
| 服务间调用(开放平台) | AppKey + 签名 | X-App-Key + X-Signature + X-Timestamp |
| 内部服务调用 | mTLS 或内网 IP 白名单 | TLS 证书层 / 网关层 |
| 回调接口(第三方通知) | HMAC 签名 | 请求头签名 |
[Requirement] 禁止在 URL 查询参数中传递 Token 或密钥(会被日志、Referer、浏览器历史记录泄漏)。
[Requirement] JWT 使用 Authorization: Bearer <token> 头传输,Token 存储于 HttpOnly Secure Cookie 或客户端内存,禁止存 localStorage 长期持有。
[Standard] JWT Payload 只存必要信息(user_id、exp、iat),禁止存入密码、权限列表等敏感或易变数据。
// JWT Payload 示例
{
"sub": "1001", // user_id
"iat": 1728894600,
"exp": 1728898200, // 1 小时过期
"jti": "a1b2c3d4" // token 唯一 ID,用于吊销
}[Caution] JWT 签名算法必须使用 HS256 以上或 RS256,禁止使用 none 算法。
[Standard] 采用双 Token 机制:Access Token 短有效期(15 分钟2 小时),Refresh Token 长有效期(730 天),仅用于换取新 Access Token。
| Token | 有效期 | 存储 | 用途 |
|---|---|---|---|
| Access Token | 15~120 分钟 | 内存 / 短期 Cookie | 接口调用 |
| Refresh Token | 7~30 天 | HttpOnly Secure Cookie | 刷新 Access Token |
[Requirement] Refresh Token 必须支持轮换(每次刷新生成新的 Refresh Token,旧的立即失效),防止重放。
[Standard] Access Token 过期返回 401 + 错误码 410002,客户端调用 /auth/refresh 获取新 Token。
[Requirement] Token 吊销通过黑名单(Redis)实现,以 jti 为 key 存储至自然过期时间,登出/改密时加入黑名单。
# 吊销 Token
def revoke_token(jti: str, exp: int):
ttl = exp - int(time.time())
if ttl > 0:
redis.setex(f"token:blacklist:{jti}", ttl, "1")[Caution] 禁止 Access Token 有效期超过 2 小时;高敏接口(支付、改密)要求 Access Token 有效期 ≤ 15 分钟,且需二次验证。
[Standard] 权限模型采用 RBAC(基于角色)+ ABAC(基于属性) 混合:角色控制功能权限,属性(部门、数据归属)控制数据权限。
用户 ──多对多── 角色 ──多对多── 权限点(如 user:read, order:create) └── 数据权限规则(如 own_dept, all)[Requirement] 所有需鉴权接口必须在网关或 Controller 入口处统一校验,禁止在业务代码中散落权限判断。
[Standard] 权限点命名:{资源}:{动作},如 user:read、order:create、payment:refund。
# 装饰器声明所需权限
@require_permission("order:read")
@router.get("/orders/{order_id}")
async def get_order(order_id: int, current_user: User = Depends(get_current_user)):
...[Standard] 数据权限通过在查询层注入过滤条件实现(如 WHERE owner_id = current_user.id 或 WHERE dept_id IN (...)),禁止在返回后过滤。
[Caution] 水平越权(A 用户访问 B 用户数据)是高频漏洞,必须通过参数校验当前用户对资源的归属权。
[Requirement] 服务间调用与开放平台接口使用 HMAC-SHA256 签名,密钥为 AppSecret,永不传输。
[Standard] 签名串由 请求方法 + 路径 + 时间戳 + 非随机串 + 排序后查询串 + 请求体哈希 拼接。
signature = HMAC-SHA256( secret=AppSecret, message="POST\n/api/v1/orders\n1728894600\nabc123\namount=100&item=book\nsha256(body)")[Requirement] 签名接口必须携带以下请求头:
| 头 | 说明 |
|---|---|
X-App-Key | 调用方标识 |
X-Timestamp | 请求时间戳(秒),服务端校验±5 分钟内 |
X-Nonce | 随机串,防重放(缓存 10 分钟) |
X-Signature | HMAC-SHA256 签名(十六进制) |
[Requirement] 服务端校验:① 时间戳在 ±5 分钟内;② Nonce 在缓存中不存在;③ 签名正确。三者均通过才放行。
def verify_signature(app_key, timestamp, nonce, signature, method, path, body):
# 1. 时间戳校验
if abs(int(time.time()) - int(timestamp)) > 300:
raise InvalidSignatureError("timestamp out of range")
# 2. Nonce 防重放
if redis.exists(f"nonce:{nonce}"):
raise ReplayAttackError("nonce already used")
redis.setex(f"nonce:{nonce}", 600, "1")
# 3. 签名校验
secret = get_secret_by_app_key(app_key)
expected = hmac.new(secret.encode(), build_string(...), sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
raise InvalidSignatureError("signature mismatch")[Caution] 签名比较必须使用 hmac.compare_digest(恒定时间),禁止 ==,防止时序攻击。
[Requirement] 生产环境强制 HTTPS(TLS 1.2+),禁用 HTTP 明文传输;敏感接口要求 TLS 1.3。
[Standard] 设置安全响应头:Strict-Transport-Security、X-Content-Type-Options: nosniff、Content-Security-Policy。
[Requirement] 所有对外接口必须在网关层做限流(按 IP、用户、AppKey 维度),默认 100 次/分钟,超限返回 429 + Retry-After 头。
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1728894660[Standard] 敏感操作(改密、支付、删除)增加二次验证(短信验证码、支付密码),并记录操作日志。
[Caution] 登录、改密、验证码接口必须加图形验证码/行为验证,防止暴力破解。
[Requirement] 日志中禁止打印 Authorization 头、Cookie、AppSecret、密码原文;Authorization 头在日志中替换为 ***。