Appearance
前端联调 · 登录认证(login.ts)
归属:前端联调 API 手册 →
src/api/login.ts(6 端点)+src/utils/request.tsrotateOnce 拦截器(1 端点) 端点:7 个 · 全部为 oauth 域 action 协议(POST /api/v1/auth/oauth) 错误码段:40101xxx/40001001—— 全表见 错误码注册表 取值语义:取值速查 · oauth ——clientId两种形态的区别、scope的分支、isNewUser/token_expiring/revoked的真实含义与缺省行为 深入说明:认证链细节(验证码三定律 / 限流三层 / clientId 语义 / 状态机)见 OAuth API Docs,本页只留接口级摘要
本页 curl 变量
bash
BASE_URL=https://api.qingmulife.cn # 本地:http://localhost:3000
ACCESS=<access_token> # rotate 换成 REFRESH
REFRESH=<refresh_token>
ENV=Prd # Dev | Prd(大小写不敏感)—— 全程必须一致⚠️ action 协议统一入口:
{"action": "<名称>", "payload": {...}}。payload必须是对象(多传字段会被忽略,非对象 →40001001)。
端点一览
| # | action | 鉴权 | 场景 |
|---|---|---|---|
| 1 | captcha | 无 | 登录页进入 / 任何失败分支后刷新图形码 |
| 2 | sms-send | 无 | 点「获取验证码」 |
| 3 | sms-login | 无 | 手机号登录 / 注册 |
| 4 | rotate | Bearer refresh | 拦截器自动续期 |
| 5 | mySessions | Bearer access | 设备会话列表页 |
| 6 | revokeSession | Bearer access | 踢掉某个设备 |
| 7 | logout | Bearer access | 设置页退出登录 ⚠️ 有已知缺陷 |
captcha
一句话:取图形验证码 —— 登录页进入时拉一次,任何失败分支后都要重拉(一次性,成败皆消费)。
curl
bash
curl -s "$BASE_URL/api/v1/auth/oauth" \
-H 'Content-Type: application/json' \
-H 'X-Env: Prd' \
-d '{"action":"captcha","payload":{}}'请求
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
action | string | ✔ | 固定 "captcha" |
payload | object | ✖ | 忽略(captcha 无入参),传 {} |
响应 data
| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
captchaId | string (UUID) | 否 | 凭据,120s 有效 + 一次性;sms-send 要回传 |
png | string | 否 | 完整 dataURL(data:image/png;base64,…)—— 前端 :src 直接渲染原生 <image> |
svg | string | 否 | SVG 源码,仅回退用途 |
⚠️ 渲染三定律(真机实测,勿再踩):① 不要用
svg-icon组件承载验证码(APP 端 renderjs 对动态 svg/dataURL 均渲染失败);② Android 原生 image 不渲染data:image/svg+xml;③ 唯一正解 = 原生<image>+png字段 + 后端 ≥ V2.0.43。详见 OAuth actions#captcha。
错误码
| 业务码 | HTTP | 触发条件 | 前端建议处置 |
|---|---|---|---|
40101005 | 200 | 同 IP 60s 内超 30 次 | 提示「操作过于频繁」,倒计时禁用 |
40001001 | 400 | payload 不是对象 | 修正请求体 |
50001001 | 200 | 服务异常 | toast + 重试 |
sms-send
一句话:发短信验证码 —— 登录页 / 绑定手机页点「获取验证码」。
curl
bash
curl -s "$BASE_URL/api/v1/auth/oauth" \
-H 'Content-Type: application/json' \
-H 'X-Env: Prd' \
-d '{"action":"sms-send","payload":{
"phone":"13800138000",
"captchaId":"<captchaId>",
"captchaCode":"ABCD",
"clientId":"android:550e8400-e29b-41d4-a716-446655440000"
}}'请求
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
action | string | ✔ | 固定 "sms-send" |
payload.phone | string | ✔ | 手机号 |
payload.captchaId | string | ✔ | 上一步 captcha 返回的凭据 |
payload.captchaCode | string | ✔ | 用户填的图形码 |
payload.clientId | string | ✔ | {类型}:{设备UUID} —— 见下方坑位;缺省回退 "default" |
🔴
clientId复合格式只允许一个冒号:android:550e8400-…✔ /android:a:b✘。 写成三段式被拒时返回的是通用异常码40201007("服务异常")而不是参数错误 —— 排障时看到40201007先怀疑clientId格式。 正则:^[A-Za-z0-9_-]{1,20}:[A-Za-z0-9-]{1,40}$。台账 #3发码与登录必须用同一个
clientId(短信码按(phone, clientId)隔离存储)。
响应 data
| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
ok | boolean | 否 | 恒 true —— 失败是抛业务码,不会返 false |
错误码
| 业务码 | HTTP | 触发条件 | 前端建议处置 |
|---|---|---|---|
40101004 | 200 | 图形码 > 120s | 静默刷新 captcha,提示已过期 |
40101003 | 200 | 图形码错误 | 刷新 captcha + 清空输入框 |
40101005 | 200 | 限流三层(IP 30/min · 手机号 60s/1 · 手机号 24h/10) | 按 msg 区分文案,倒计时保持禁用 |
40101006 | 200 | 短信通道故障 / 凭据未配置 | toast「短信通道暂不可用」+ 重拉 captcha |
40201007 | 200 | clientId 格式非法(常见) | 不要当后端故障 —— 检查 clientId 是否多冒号 |
40001001 | 400 | 入参校验失败 | 修正表单 |
sms-login
一句话:手机号登录 / 注册(不存在的号码自动建号)—— 登录页点「登录」。
curl
bash
curl -s "$BASE_URL/api/v1/auth/oauth" \
-H 'Content-Type: application/json' \
-H 'X-Env: Prd' \
-d '{"action":"sms-login","payload":{
"phone":"13800138000",
"smsCode":"1234",
"clientId":"android:550e8400-e29b-41d4-a716-446655440000",
"agreementAccepted":true,
"deviceName":"常智的 Android"
}}'请求
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
action | string | ✔ | 固定 "sms-login" |
payload.phone | string | ✔ | 手机号 |
payload.smsCode | string | ✔ | 短信验证码 |
payload.clientId | string | ✔ | 复合格式同 sms-send(必须与发码时一致) |
payload.agreementAccepted | boolean | ✔ | 必须字面 true,否则 40101008 |
payload.nickname | string | ✖ | 新用户不传则自动生成「用户XXXX」 |
payload.deviceName | string | ✖ | 会话展示名(设备会话列表显示用) |
响应 data
| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
tokens.access | string (JWT) | 否 | access token |
tokens.refresh | string (JWT) | 否 | refresh token(rotate 用) |
expiresAt.access | string ISO | 否 | access 过期时刻 |
expiresAt.refresh | string ISO | 否 | refresh 过期时刻 |
user.id | string | 否 | 用户 id(前端存 storage 当 userId) |
user.nickname | string | 否 | 昵称 |
user.phone | string | null | 是 | 脱敏(138****8000)—— 前端不消费 |
user.isNewUser | boolean | 否 | 注册即 true(仅本次响应) —— 前端弹「完善基础资料」向导的依据 |
token_expiring | boolean | 否 | ⚠️ snake_case 孤儿键(与 camelCase 契约风格不一致),前端未消费 |
错误码
| 业务码 | HTTP | 触发条件 | 前端建议处置 |
|---|---|---|---|
40101001 | 200 | 短信码不存在 / 过期 / 已被 3 次错作废 | 「验证码已失效」→ 回 sms-send 重发 |
40101003 | 200 | 短信码错误 | 「还剩 X 次机会」,接近 3 次时警告 |
40101008 | 200 | agreementAccepted 非字面 true | 提示勾选协议 |
40101013 | 403 | 账号被封禁 | 「账号已被封禁,请联系客服」,禁止重试 |
40201007 | 200 | clientId 格式非法 | 同 sms-send —— 先查格式 |
40001001 | 400 | 入参校验失败 | 修正表单 |
rotate
一句话:用 refresh 换新的 access(+ 新的 refresh)—— 由 request.ts 的 rotateOnce 单飞拦截器自动调用,不是业务直调。
curl
bash
curl -s "$BASE_URL/api/v1/auth/oauth" \
-H 'Content-Type: application/json' \
-H 'X-Env: Prd' \
-H "Authorization: Bearer $REFRESH" \
-d '{"action":"rotate","payload":{}}'请求
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
action | string | ✔ | 固定 "rotate" |
payload | object | ✖ | 忽略(无入参) |
Authorization | header | ✔ | 🔴 Bearer <refresh_token> —— 不是 access |
X-Env | header | ✔ | 双库路由必需(随主请求一致,否则查不到绑定行) |
响应 data
| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
tokens.access | string | 否 | 新 access(写 storage.token) |
tokens.refresh | string | 否 | 旋转后的新 refresh(旧的立即失效,务必覆盖存储) |
expiresAt.access / expiresAt.refresh | string ISO | 否 | ✅ 2026-09-20 契约修复后已返回,与 sms-login 同口径同形状 |
user.id | string | 否 | 仅 id —— 与 sms-login 的 user 不同构(无 nickname/phone),共享 user 类型的地方会踩 |
token_expiring | boolean | 否 | snake_case 孤儿键 |
错误码
| 业务码 | HTTP | 触发条件 | 前端建议处置 |
|---|---|---|---|
40101011 | 401 | 验签失败 / binding 非 active / 密文缺失 / 用户不存在 / 传了 access 而非 refresh | 「登录已失效」→ 清空本地会话进登录页,禁止重试 |
40101012 | 401 | 重放(旧 refresh 二次使用)—— 服务端已连坐吊销该设备会话 | 唯一动作:清会话进登录页 |
40101014 | 401 | 设备信任 90 天窗口过期(滑动) | 引导重新登录验证 |
50001001 | 200 | 服务异常 | 可重试一次 |
mySessions
一句话:当前账号的活跃设备会话列表 —— 设备会话页(mine/account-security/device-sessions.vue)。
curl
bash
curl -s "$BASE_URL/api/v1/auth/oauth" \
-H 'Content-Type: application/json' \
-H 'X-Env: Prd' \
-H "Authorization: Bearer $ACCESS" \
-d '{"action":"mySessions","payload":{}}'请求
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
action | string | ✔ | 固定 "mySessions" |
Authorization | header | ✔ | Bearer <access>(须为 oauth_access 且未被 denylist) |
响应 data
| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
sessions[].clientId | string | 否 | 会话键,如 android:{uuid} |
sessions[].deviceName | string | null | 是 | 登录时传的 deviceName |
sessions[].lastActiveAt | string ISO | 否 | last_rotated_at || created_at |
sessions[].expiresAt | string ISO | null | 是 | 会话过期时刻 |
sessions[].current | boolean | 否 | 是否当前设备(token cid 比对) |
✅ 全表最干净的契约 —— 前后端字段名 / 可空性完全一致。
revokeSession的target直接传sessions[].clientId。
错误码
| 业务码 | HTTP | 触发条件 | 前端建议处置 |
|---|---|---|---|
40101011 | 401 | 缺 / 非法 Authorization、非 access token、token 已吊销 | 走拦截器 401 分档 |
50001001 | 200 | token service 未装配 / 服务异常 | toast + 重试 |
revokeSession
一句话:踢掉指定设备会话 —— 设备会话列表页点「下线」。
curl
bash
curl -s "$BASE_URL/api/v1/auth/oauth" \
-H 'Content-Type: application/json' \
-H 'X-Env: Prd' \
-H "Authorization: Bearer $ACCESS" \
-d '{"action":"revokeSession","payload":{"target":"android:550e8400-e29b-41d4-a716-446655440000"}}'请求
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
action | string | ✔ | 固定 "revokeSession" |
payload.target | string | ✔ | 目标会话键 —— 页面传 s.clientId(旧字段名 clientId 也兼容;缺 → 40001001) |
响应 data
| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
revoked | string | 否 | 被吊销的 target 回显(前端不消费,成功即 toast) |
错误码
| 业务码 | HTTP | 触发条件 | 前端建议处置 |
|---|---|---|---|
40101015 | 401 | 目标会话不存在或已下线 | toast,不续期 —— 前端白名单已精确匹配 ✅ |
40001001 | 400 | 缺 target | 检查调用点 |
40101011 | 401 | Authorization 缺失 / 非法 | 走拦截器 401 分档 |
logout
一句话:退出登录 —— 设置页用户主动退出。
🔴 已知缺陷(决策:联调期再修) —— 请看下面的「坑位」再动手。
curl
bash
# 现状(前端 src/api/login.ts 的写法)—— 吊销的是 oauth:default,no-op
curl -s "$BASE_URL/api/v1/auth/oauth" \
-H 'Content-Type: application/json' \
-H 'X-Env: Prd' \
-H "Authorization: Bearer $ACCESS" \
-d '{"action":"logout","payload":{}}'
# 修复后的写法(前端补传 clientId)
curl -s "$BASE_URL/api/v1/auth/oauth" \
-H 'Content-Type: application/json' \
-H 'X-Env: Prd' \
-H "Authorization: Bearer $ACCESS" \
-d '{"action":"logout","payload":{"clientId":"android:550e8400-e29b-41d4-a716-446655440000"}}'请求
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
action | string | ✔ | 固定 "logout" |
payload.scope | string | ✖ | device(默认)| all;非法 → 40001001 |
payload.clientId | string | ✖ | 🔴 当前前端不传 → 后端取默认 "default" → 吊销 oauth:default(不存在的会话)→ no-op |
Authorization | header | ✔ | Bearer <access> —— access 本体确实会被 jti denylist 即时失效 |
响应 data
| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
ok | boolean | 否 | 恒 true |
scope | string | 否 | 回显 "device" |
revoked | number | 否 | ⚠️ scope=device 时恒 1 —— 即使没吊销到任何行。此场景下该值会误导(显示"成功吊销 1 个") |
scope=all 时为实际吊销数 |
错误码
| 业务码 | HTTP | 触发条件 | 前端建议处置 |
|---|---|---|---|
40001001 | 400 | scope 非 device/all | 检查调用点 |
50001001 | 200 | 服务异常 | 本地清 token 已足够,可不阻塞 |
🔴 坑位 · logout 的 no-op 链路(已独立验证,非推测)
前端 login.ts:69 → { action:'logout', payload:{} } ← payload 空
后端 oauth.py 默认值 → scope='device' · client_id='default'
logout.py:55 → cz_revoke_by_cid(user_id, 'default')
真实会话行 → 'oauth:android:{uuid}' ≠ 'default'
→ 吊销不存在的会话,真实会话未动后果:
- 服务端该设备会话仍 active(
mySessions仍显示在线) - 该设备 refresh token 登出后仍可继续
rotate换新 access (access 本体被 denylist 即时失效 ✅ —— 部分失效)
前端本地清 token 掩盖了这一点。后端 logout.py:3 代码注释自己写着「⚠️ scope=device 不传 clientId 会吊销 oauth:default(陷阱)」。
候选修法(联调期决策):
- A:前端
payload补传clientId(它本地存了)→ 后端语义不变 - B:后端改语义(
scope=device且无clientId→ 吊销该用户全部会话)→ 更安全但会误踢其他设备
完整决策记录见 → 联调问题台账。