docs: 第三波交付收口——认证契约与两端实现报告入档

- 新增 API 契约:docs/api/openapi.yaml(T6a 正式化)与契约说明页(契约先行原则)
- 入档报告 16(后端 JWT 会话,37→73 测试)与 17(Flutter 登录纵切,7→30 测试)
- 进展看板更新至第三波完成,第四波为联调 E2E → CI → 编排 → 埋点
- 门禁:mkdocs build --strict 通过
This commit is contained in:
2026-09-04 12:18:29 +08:00
parent 18746ce6fc
commit 273064c10d
6 changed files with 606 additions and 13 deletions
+411
View File
@@ -0,0 +1,411 @@
openapi: 3.0.3
info:
title: Patbond API — Auth & Me(第一批公开接口)
version: 1.0.0
description: |
Patbond 第一迭代「真实登录纵切」公开契约(冻结稿的正式化,字段与草案零偏差)。
## 通用约定(development-plan 第 6 节)
- 公开接口统一前缀 `/api/v1`JSON 字段一律 `camelCase`;资源 ID 为 UUID 字符串。
- 所有时间字段为 ISO 8601 且带时区偏移(如 `2026-09-04T04:05:06.789Z`)。
- 统一响应信封 `{"code": 0, "message": "success", "data": …}`;错误同时携带正确的
HTTP 状态码与稳定业务码,业务码永不复用或改号。
- `/internal/**` 为服务间接口,不属于本公开契约,需 `X-Internal-Token` 服务凭证,
未携带或错误一律 401。
## 错误码表
| 业务码 | HTTP | 场景 |
| --- | --- | --- |
| 0 | 200 | 成功 |
| 40000 | 400 | 参数校验失败(含 JSON 不可解析;message 为首个字段错误) |
| 40100 | 401 | 用户名或密码错误 |
| 40101 | 401 | access token 无效或过期(缺失、伪造、篡改、过期) |
| 40102 | 401 | refresh token 已失效或被重用(未知、过期、已轮换、已退出、家族已撤销) |
| 40400 | 404 | 资源不存在 |
| 40900 | 409 | 用户名已存在(大小写不敏感) |
| 40901 | 409 | 手机号已被使用 |
| 42300 | 423 | 登录失败次数过多,账号已临时锁定(见下) |
| 50000 | 500 | 服务器内部错误 |
| 50300 | 503 | 依赖服务暂不可用 |
## 会话模型(ADR-003,数值均为服务端配置项)
- access tokenJWTRS256),有效期 15 分钟;由资源服务用公钥本地验签。
- refresh token:不透明随机串,有效期 30 天;**每次刷新即轮换**,旧值立即失效。
- 已轮换/已失效的 refresh token 再次被使用时,判定为重用,**整个 token family
(该登录会话链)全部撤销**,持有者需重新登录。
- 允许多设备并行会话;退出仅撤销当前会话(由所提交的 refreshToken 标识),
其他设备不受影响。已签发的 access token 在剩余有效期内仍可用。
- 登录失败限制:同一账号在 15 分钟窗口内密码错误累计 5 次(配置项),账号锁定
15 分钟;锁定期间即使密码正确也返回 423/42300;一次成功登录重置计数窗口。
servers:
- url: http://127.0.0.1:8081
description: patbond-auth(本地开发,/api/v1/auth/**
- url: http://127.0.0.1:8082
description: patbond-user(本地开发,/api/v1/me
tags:
- name: auth
description: 注册 / 登录 / 刷新 / 退出(patbond-auth
- name: user
description: 当前用户(patbond-user
paths:
/api/v1/auth/register:
post:
tags: [auth]
summary: 注册并创建会话
operationId: register
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RegisterRequest'
responses:
'200':
description: 注册成功,返回令牌对
content:
application/json:
schema:
$ref: '#/components/schemas/AuthTokenEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'409':
description: 用户名或手机号已被占用(code 40900 / 40901
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
usernameTaken:
value: { code: 40900, message: 用户名已存在, data: null }
phoneTaken:
value: { code: 40901, message: 手机号已被使用, data: null }
/api/v1/auth/login:
post:
tags: [auth]
summary: 登录并创建会话
description: 多设备并行:每次登录开启独立会话(独立 token family),互不影响。
operationId: login
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LoginRequest'
responses:
'200':
description: 登录成功,返回令牌对
content:
application/json:
schema:
$ref: '#/components/schemas/AuthTokenEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
description: 用户名或密码错误(code 40100)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
invalidCredentials:
value: { code: 40100, message: 用户名或密码错误, data: null }
'423':
description: 登录失败次数过多,账号临时锁定(code 42300)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
locked:
value: { code: 42300, message: 登录失败次数过多,账号已临时锁定, data: null }
/api/v1/auth/refresh:
post:
tags: [auth]
summary: 轮换 refresh token
description: |
成功时返回全新令牌对,旧 refreshToken 立即失效(轮换)。提交已轮换或已失效的
refreshToken 返回 401/40102,且视为重用攻击:该 token family 的全部会话被撤销。
operationId: refresh
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RefreshRequest'
responses:
'200':
description: 轮换成功,返回新的令牌对
content:
application/json:
schema:
$ref: '#/components/schemas/AuthTokenEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
description: refresh token 已失效或被重用(code 40102
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
invalidated:
value: { code: 40102, message: refresh token 已失效或被重用, data: null }
/api/v1/auth/logout:
post:
tags: [auth]
summary: 退出(撤销当前会话)
description: |
撤销 body 中 refreshToken 对应的会话;其他设备的会话不受影响(ADR-003)。
需携带有效的 access token(从中取用户身份,防止跨账号撤销)。幂等:对已
失效的 refreshToken 仍返回成功。
operationId: logout
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LogoutRequest'
responses:
'200':
description: 已退出
content:
application/json:
schema:
$ref: '#/components/schemas/VoidEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
/api/v1/me:
get:
tags: [user]
summary: 当前用户资料
description: 由 patbond-user 提供;access token 以 RS256 公钥本地验签,无需经过 auth 服务。
operationId: me
security:
- bearerAuth: []
responses:
'200':
description: 当前用户
content:
application/json:
schema:
$ref: '#/components/schemas/MeEnvelope'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
description: 用户不存在(如已注销;code 40400)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: 'Authorization: Bearer <accessToken>RS256 JWT'
responses:
ValidationError:
description: 参数校验失败(code 40000
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
validation:
value: { code: 40000, message: 参数校验失败, data: null }
AccessTokenInvalid:
description: access token 缺失、无效或过期(code 40101)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
tokenInvalid:
value: { code: 40101, message: token 无效或过期, data: null }
schemas:
RegisterRequest:
type: object
required: [username, password]
properties:
username:
type: string
minLength: 3
maxLength: 32
description: 用户名,大小写不敏感唯一
example: demo_user
phone:
type: string
pattern: '^\+[1-9][0-9]{7,14}$'
description: 手机号,E.164 格式;可选,唯一
example: '+8613800138000'
password:
type: string
format: password
minLength: 6
maxLength: 64
example: secret123
nickname:
type: string
minLength: 1
maxLength: 32
description: 昵称;可选(冻结稿之外的可选扩展字段,前端可忽略)
example: 小柴
LoginRequest:
type: object
required: [username, password]
properties:
username:
type: string
example: demo_user
password:
type: string
format: password
example: secret123
RefreshRequest:
type: object
required: [refreshToken]
properties:
refreshToken:
type: string
description: 当前持有的 refresh token(不透明随机串)
example: Zx3v…43位base64url…Qk
LogoutRequest:
type: object
required: [refreshToken]
properties:
refreshToken:
type: string
description: 要撤销的当前会话的 refresh token
example: Zx3v…43位base64url…Qk
AuthTokens:
type: object
description: 注册 / 登录 / 刷新共用的令牌对(冻结契约,恰好这 6 个字段)
required:
- userId
- tokenType
- accessToken
- accessTokenExpiresAt
- refreshToken
- refreshTokenExpiresAt
properties:
userId:
type: string
format: uuid
example: 019212aa-0000-7000-8000-000000000001
tokenType:
type: string
enum: [Bearer]
example: Bearer
accessToken:
type: string
description: RS256 JWT,有效期 15 分钟(配置项)
example: eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI…
accessTokenExpiresAt:
type: string
format: date-time
description: ISO 8601 带时区
example: '2026-09-04T04:20:06.789Z'
refreshToken:
type: string
description: 不透明随机串,有效期 30 天(配置项),每次刷新轮换
example: Zx3v…43位base64url…Qk
refreshTokenExpiresAt:
type: string
format: date-time
example: '2026-10-04T04:05:06.789Z'
Me:
type: object
description: 当前用户资料(冻结契约,恰好这 4 个字段)
required: [userId, username, createdAt]
properties:
userId:
type: string
format: uuid
example: 019212aa-0000-7000-8000-000000000001
username:
type: string
example: demo_user
phone:
type: string
nullable: true
description: E.164;未绑定时为 null
example: '+8613800138000'
createdAt:
type: string
format: date-time
example: '2026-09-04T04:05:06.789Z'
AuthTokenEnvelope:
type: object
required: [code, message]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
$ref: '#/components/schemas/AuthTokens'
MeEnvelope:
type: object
required: [code, message]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
$ref: '#/components/schemas/Me'
VoidEnvelope:
type: object
required: [code, message]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
nullable: true
example: null
ErrorEnvelope:
type: object
required: [code, message]
properties:
code:
type: integer
description: 稳定业务错误码(见顶部错误码表)
example: 40101
message:
type: string
example: token 无效或过期
data:
nullable: true
example: null