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 下类型 不可用。
37 lines
1.3 KiB
Python
Executable File
37 lines
1.3 KiB
Python
Executable File
"""identity 域的请求/响应模型(T4:双令牌会话,FR-ACC-05)。
|
|
|
|
``apps/api/schemas.py`` 是本批次的共享禁改文件,refresh/logout 的契约模型
|
|
放在域内这里,由 ``apps/api/routers/auth.py`` 引用;wire 响应仍只是统一
|
|
信封 (架构文档 §9.2) 里的 ``data`` 部分。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
#: 生成端 token_urlsafe(48) 恒为 64 字符;上限放宽到 128 容忍未来换编码。
|
|
_REFRESH_TOKEN_MAX_LENGTH = 128
|
|
|
|
|
|
class RefreshRequest(BaseModel):
|
|
refresh_token: str = Field(min_length=1, max_length=_REFRESH_TOKEN_MAX_LENGTH)
|
|
|
|
|
|
class LogoutRequest(BaseModel):
|
|
refresh_token: str = Field(min_length=1, max_length=_REFRESH_TOKEN_MAX_LENGTH)
|
|
|
|
|
|
class TokenPairResponse(BaseModel):
|
|
"""dev-token 与 /auth/refresh 共用的双令牌响应。
|
|
|
|
旧字段名 ``access_token/token_type/expires_in/user_id`` 保持不变
|
|
(smoke.sh 与既有测试依赖),新增 ``refresh_token/refresh_expires_in``。
|
|
"""
|
|
|
|
access_token: str
|
|
refresh_token: str
|
|
token_type: str = "Bearer"
|
|
expires_in: int = Field(description="access token 有效期(秒)")
|
|
refresh_expires_in: int = Field(description="refresh token 有效期(秒)")
|
|
user_id: str = Field(description="雪花 ID 以字符串返回,避免 JS 精度丢失")
|