Files
meida_backend/packages/identity/tokens.py
T
xsl 140a63effa feat(identity): refresh token rotation with server-side revocable sessions
id_session 落地:opaque refresh(token_urlsafe(48))+ sha256 哈希存储 +
一次性轮换(旧行撤销与新行同一事务)+ logout 幂等撤销 + 已撤销重放检测
告警(V1 不做家族连坐);错误码 40103/40104/40105 接入。新增 POST
/auth/refresh、POST /auth/logout;dev-token 扩展为双令牌,旧字段
access_token/token_type/expires_in/user_id 原名保留,新增
refresh_token/refresh_expires_in(smoke.sh 不受影响)。

测试:tests/unit/test_refresh_tokens.py 覆盖 token 长度/字符域/唯一性、
哈希稳定性与不可逆(7 例,不连网不连库);tests/integration/
test_auth_refresh.py 覆盖轮换后新 access 可调 /quota、旧 refresh 二次
使用 40105、logout 后失效 40105、伪造 40103、TTL=0 过期 40104(5 例)。
门禁全绿:ruff check/format、mypy 两段、lint-imports 4 契约、
单测 176 通过、集成 8 通过。

偏差说明:1) SessionIssued 较任务文档字段清单增加 user_id ——
/auth/refresh 换发 access JWT 必须知道用户,opaque refresh 本身不携带
信息,只能随签发结果带回;2) 过期分支「顺手落库 revoked=true」在 HTTP
路径会随 401 的统一回滚一起丢失(仅事务内标记,已注释说明,不影响判定
语义);3) repo 撤销用 SELECT FOR UPDATE + ORM 变更而非 UPDATE
rowcount —— 对齐 billing.repo 现行模式,且 rowcount 在 mypy 下类型
不可用。
2026-08-14 16:21:52 +08:00

31 lines
1.2 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Opaque refresh tokens (FR-ACC-05, 架构文档 §6.2 id_session / §9.1).
为什么是 opaque 随机串而不是 JWT:refresh 的核心价值就是「服务端可单条撤销」
(FR-ACC-05),无状态 JWT 天生做不到撤销——除非引入黑名单,而黑名单本身就是把
状态搬回服务端,那不如直接用查表就能撤销的 opaque 令牌。
库里只存 sha256 哈希而非明文:查库校验时重算一次哈希即可比对,而泄漏数据库
不等于泄漏令牌——sha256 不可逆,拿到哈希换不出新 access。
本模块是纯函数(无 IO、无配置),方便穷举式单测。
"""
from __future__ import annotations
import hashlib
import secrets
#: token_urlsafe(48) 输出 64 个 URL 安全字符,携带 384 bit 熵,
#: 暴力枚举哈希空间在计算上不可行。
_TOKEN_BYTES = 48
def generate_refresh_token() -> str:
"""生成新 refresh token。明文只在签发瞬间出现一次,此后系统内只见哈希。"""
return secrets.token_urlsafe(_TOKEN_BYTES)
def hash_refresh_token(token: str) -> str:
"""sha256 hex(64 字符,与 id_session.refresh_token_hash 列宽一致)。"""
return hashlib.sha256(token.encode()).hexdigest()