---
url: /docs/api-docs/v1/oauth.md
---
# OAuth 登录认证（v1）

> 统一入口：`POST {BASE_URL}/api/v1/auth/oauth`
> body = `{ "action": "<9种之一>", "payload": { ... } }`
> 鉴权头见下方「鉴权头」表
> Swagger 调试：`{BASE_URL}/docs`

## 9 个 Action 一览

| Action | 鉴权 | 触发场景 | 详细 |
|---|---|---|---|
| `captcha` | 无 | 登录页进入 / 任意失败分支 | [→ actions#captcha](/api-docs/v1/oauth/actions#captcha) |
| `sms-send` | 无 | 点「获取验证码」 | [→ actions#sms-send](/api-docs/v1/oauth/actions#sms-send) |
| `sms-verify` | 无 | 分步表单预校验 | [→ actions#sms-verify](/api-docs/v1/oauth/actions#sms-verify) |
| `sms-login` | 无 | 手机号登录/注册 | [→ actions#sms-login](/api-docs/v1/oauth/actions#sms-login) |
| `callback` | 无 | 微信 SDK 回调 | [→ actions#callback](/api-docs/v1/oauth/actions#callback) |
| `rotate` | **Bearer refresh** | 401 / token\_expiring | [→ actions#rotate](/api-docs/v1/oauth/actions#rotate) |
| `logout` | Bearer access | 设置页退出 | [→ actions#logout](/api-docs/v1/oauth/actions#logout) |
| `bind-phone` | Bearer access | 微信用户补手机号 | [→ actions#bind-phone](/api-docs/v1/oauth/actions#bind-phone) |
| `unbind` | Bearer access | 解绑微信 | [→ actions#unbind](/api-docs/v1/oauth/actions#unbind) |

## 鉴权头（哪些 action 要带）

| action | Header | 说明 |
|---|---|---|
| captcha / sms-send / sms-verify / sms-login / callback | 无 | 公开 |
| rotate | `Authorization: Bearer <refresh_token>` | **注意不是 access** |
| logout / bind-phone / unbind | `Authorization: Bearer <access_token>` | |

## 公共约定（先读这节，后面不重复）

### 统一包络

* **成功**：`{ "code": 0, "data": { ... }, "msg": "ok" }`
* **业务失败**：HTTP 200 + `{ "code": "40101xxx", "msg": "..." }`（[错误码矩阵](/api-docs/v1/oauth/error-codes)）
* **入参校验失败**：HTTP 400 + `{ "msg": "<字段路径>: <原因>" }`（如 `"phone: invalid phone"`）

### clientId —— 服务端会话身份证

**两种合法形态**（二选一，都对）：

| 形态 | 正则 | 说明 |
|---|---|---|
| **复合（推荐 · 设备级隔离）** | `^[A-Za-z0-9_-]{1,20}:[A-Za-z0-9-]{1,40}$` | `{类型}:{设备UUID}`，如 `android:550e8400-e29b-41d4-a716-446655440000` |
| 简单（存量） | `^[A-Za-z0-9_-]{1,32}$` | 无冒号，类型级共享（如 `default` / `android-phone`） |

> ⚠️ **复合格式只允许一个冒号**。写成 `android:foo:bar` 会被拒 —— 且拒绝方式是**通用异常码
> `40201007`（"服务异常"）**，不是参数错误提示（见[联调问题台账 #3](/internal-wiki/operations/frontend-integration-issues)）。
> 排障时看到 40201007 先怀疑 clientId 格式。

* 服务端织入：Redis key（短信码隔离）、JWT `cid` claim、DeviceBinding `vendor`（`oauth:{clientId}`）
* **语义**：同 `(userId, clientId)` 在服务端只有一个活跃会话 —— 同 clientId 重新登录会**顶掉**旧会话（旧 refresh 立即失效）。多设备共存请给不同 clientId（如 `android:pad-uuid` / `android:phone-uuid`）
* **发码与登录必须用同一个 clientId**（短信码按 `(phone, clientId)` 隔离存储）

### IP 相关行为（真机必知）

* `clientIp` 取 `x-forwarded-for` 首段（nginx 代理后）—— 真机换网（WiFi↔4G）IP 变化会使**未消费的图形验证码失效**（绑定生成时 IP /24 段），刷新重取即可
* 限流按此 IP 计

## 业务场景地图（页面 × API 矩阵）

| uniapp 页面 / 组件 | 调用的 action | 调用时机 |
|---|---|---|
| **App 冷启动**（App.vue） | `rotate` | 本地有 refresh\_token 时静默续期；失败清存储进登录页 |
| **登录页 · 图形验证码组件** | `captcha` | 进入页面 1 次；任何失败分支后刷新 |
| **登录页 · 手机号登录 Tab** | `sms-send` → `sms-login` | 点「获取验证码」→ 点「登录」 |
| **绑定手机页（分步表单）** | `sms-verify` | 可选：提交 bind-phone 前预验证短信码 |
| **登录页 · 微信登录按钮** | `callback` | 微信 SDK 授权回调解出 code 后 |
| **个人中心 · 绑定手机号** | `sms-send` + `bind-phone` | 微信登录用户补手机号 |
| **个人中心 · 解绑微信** | `unbind` | 账号管理页 |
| **设置页 · 退出登录** | `logout` | 用户主动退出（scope=device / all） |
| **全局 HTTP 拦截器** | `rotate` | 收到 401 或 `token_expiring=true` 时 |

## 详细子页

* [9 Action 完整字段表](/api-docs/v1/oauth/actions)
* [错误码矩阵（40101xxx）](/api-docs/v1/oauth/error-codes)
* [验证码状态机（captcha + sms-code）](/api-docs/v1/oauth/state-machine)

## 实现参考

后端实施规范见 [🟢 Internal Wiki → OAuth 实施总览](/internal-wiki/architecture/oauth-implementation)。
