Files
wecom_it_smart_desk/docs/04-运维文档/部署运维/06-OTP二次验证实现.md
T
Simon facc04aa65 chore: docs 结构整改 + compose 双目录对齐(合并重建提交)
本提交为 .git 对象库损坏后的重建提交,内容等价于原先三个本地提交
(5e2fd4c2 / 57a53c98 / 5d7e1873)的累积结果,未做任何额外改动。

一、docs 结构整改(整改 #14)
根因:重构时新结构为 untracked 文件,执行 git stash(未带 -u)未纳入,
随后 git reset 拉回 HEAD 旧 tracked 树,导致旧树复活、新旧两棵目录
树并存于 docs/,共 791 文件、双分类体系冲突。

修复动作:
- b2 同名异主题文件改名迁移保全 9 个
- C 类 39 个孤立文件按主题正确归类
- A/B1 类 222 个重复文件删除(新结构已有内容副本)
- 9 个旧独有空目录删除
- 270 处内部引用按 verified 映射改写
- 整改记录 #14 登记于 04-运维文档/部署运维

结果:docs 791 → 569 文件,顶层仅规范 8 类 + 治理文件,单树恢复。
残留:约 20 处指向从未存在文件的陈旧死链,归入独立文档卫生任务。

二、compose 双目录对齐(消除踩坑 A)
- docker-compose.yml:nginx 前端挂载全部由根目录 frontend-*/dist
  改为 src/frontend-*/dist(h5 / agent / admin / terminal)
- docker-compose.dev.yml:dev 服务 build context 与卷同步改 src/
- 效果:本地 docker compose up 不再把根目录 stale dist 挂回,
  与线上一致,分叉隐患消除(已 docker compose config 校验通过)

防复发铁律:
- 重构须提交;仓库修复须 git stash -u 或先 commit
- 新结构须 git add 并提交,避免再次 untracked 复活
- H5 改动只动 src/frontend-h5/,禁改根目录遗留 frontend-*/
2026-08-07 22:31:32 +08:00

132 lines
7.1 KiB
Markdown
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.
# OTP 二次验证实现文档
> **关联文档(认证模块三件套)**:本文档为运维侧实现说明,与以下文档形成闭环 ——
> - PRD`../../01-产品文档/01-认证与登录/PRD-REQ-认证-统一认证与登录-v1.1.md`
> - 技术方案:`../../02-技术文档/技术方案-REQ-认证-统一认证与登录-v1.0.md`
> - 测试用例:`../../03-测试文档/03-功能测试用例/TC-REQ-认证-统一认证与登录-v1.0.md`
>
> **版本对齐说明(2026-08-05 修正)**:本文档原始内容描述的是 **v0.5.6 时期的 `otp_*` 旧设计**`/api/agents/otp-*` + `otp_secret`/`otp_enabled` 字段)。该设计已在 **AUTH-03 三端认证重构(v0.7.x)** 中被统一取代:
> - 端点前缀由 `/api/agents/otp-*` 改为 **`/api/auth/otp-*`**(路由文件 `app/api/otp.py`prefix=`/auth`);
> - 模型字段由 `otp_secret`/`otp_enabled` 改为 **`mfa_secret`/`mfa_enabled`/`mfa_bound_at`/`mfa_last_verified_at`**migration 023 新增、026 删除旧 `otp_*` 字段);
> - 旧 `otp_secret`/`otp_enabled` 字段已于 **migration 026v0.7.1** 彻底删除。
>
> 本次修正已将上述内容对齐到**当前生产实现**(以 `app/api/otp.py` + `app/api/agents.py` 为准)。注意:部署指南 `07-扫码登录OTP部署指南-v0.7.0.md` 与测试用例仍引用更早的 `/api/mfa/*` 规划命名,同样需后续对齐到 `/api/auth/otp-*`(见技术方案 §2.3)。
## 功能概述
为 IT 支持服务台坐席端增加 OTP 二次验证(MFA)功能:
- admin 角色登录时需要输入 Google Authenticator / 兼容 TOTP 应用的动态码
- 首次登录且未绑定时,引导完成首次绑定(不签发 token)
- OTP 丢失后由管理员重置
## 实现方案
### 1. 后端修改
#### 1.1 安装依赖
```bash
pip install pyotp qrcode[pil] pillow
```
#### 1.2 数据库模型
`agents` 表当前 MFA 相关字段(见 `app/models/agent.py`,由 migration 023 新增):
- `mfa_secret`: TOTP 共享密钥(Base32 编码,绑定时生成,验证启用前不算启用)
- `mfa_enabled`: MFA 是否启用(默认 false,首次验证成功后置 true)
- `mfa_bound_at`: 首次绑定完成时间(可空,用于审计与回收策略)
- `mfa_last_verified_at`: 最近一次验证成功时间(可空,安全审计用)
> ⚠️ 历史字段 `otp_secret` / `otp_enabled` 已在 **migration 026v0.7.1** 删除,代码中不再存在,请勿沿用。
#### 1.3 Schema 修改
文件:`backend/app/schemas/agent.py`
- `AgentLogin`:含 `otp_code: Optional[str]` 字段(admin 角色登录必填,6 位数字 TOTP 动态码)
- `AgentResponse`:返回绑定状态字段(注:response schema 中字段名仍写作 `otp_enabled`,属于已知技术债,与模型 `mfa_enabled` 对应,不影响端点行为;详见技术方案 §2.3)
#### 1.4 API 修改
**OTP 管理端点**(文件:`backend/app/api/otp.py`,路由器 `prefix="/auth"`tags=["OTP二次认证"]):
| 方法 | 端点 | 说明 |
|------|------|------|
| GET | `/api/auth/otp-status` | 查询当前用户 MFA 绑定状态 |
| POST | `/api/auth/otp-bind` | 生成 TOTP secret + 二维码(写入 `mfa_secret``mfa_enabled` 保持 false |
| POST | `/api/auth/otp-verify` | 验证动态码:首次绑定(mfa_enabled=False 有 secret)→ 启用 MFA 并签发 token;已绑定 → 常规验证并写 Redis 30 分钟标记 |
| POST | `/api/auth/otp-unbind` | 用户主动解绑,清空 `mfa_secret`/`mfa_enabled`/`mfa_bound_at` |
| POST | `/api/auth/otp-admin-reset/{employee_id}` | 管理员重置指定员工 MFA(清空其 `mfa_*` 字段) |
| GET | `/api/auth/otp-admin-users` | 管理员查看全部坐席 MFA 绑定状态列表 |
**登录接口 MFA 校验**(文件:`backend/app/api/agents.py`,函数 `agent_login`):
- `mfa_enabled=True` 且未提供 `otp_code` → 返回 `require_otp: True`,前端弹 OTP 输入框
- `mfa_enabled=False`(未绑定)→ 返回 `require_otp_bind: True`,引导首次绑定流程(不签发 token)
- 提供正确 `otp_code` → 经 `MFAService.verify_code(agent.mfa_secret, otp_code)` 校验通过后签发 token
### 2. 前端修改
#### 2.1 坐席端 API
文件:`frontend-agent/src/api/agent.ts`
- `login()` 增加 `otpCode` 参数
#### 2.2 坐席端 Store
文件:`frontend-agent/src/stores/agent.ts`
- `login()` 增加 `otpCode` 参数
- 处理 `require_otp` / `require_otp_bind` 标记,驱动页面分流
#### 2.3 坐席端登录页面
文件:`frontend-agent/src/views/Login.vue`
- 增加 OTP 输入框(`v-if="requireOtp"`
- 首次登录返回 `require_otp_bind` 时引导跳转到绑定流程
#### 2.4 坐席端绑定页面(新增)
文件:`frontend-agent/src/views/MfaBind.vue`
- 展示二维码 + 密钥,输入动态码完成首次绑定
#### 2.5 高危操作 OTP 守卫(新增)
文件:`frontend-agent/src/composables/useHighRiskOtp.ts`
- 调用高危操作前若 30 分钟内未验证过 OTP,弹 OTP 输入框 → 调 `/api/auth/otp-verify` → 重试
#### 2.6 管理端 MFA 管理(新增)
文件:`frontend-admin/src/views/MfaManage.vue`
- 管理员查看坐席 MFA 绑定状态、重置指定员工 MFA
## 使用流程
### 首次绑定 OTP
1. 管理员登录坐席端(此时 `mfa_enabled=False`,返回 `require_otp_bind`
2. 跳转绑定页,调用 `POST /api/auth/otp-bind` 获取二维码与密钥
3. 使用 Google Authenticator / 兼容 TOTP 应用扫描二维码
4. 调用 `POST /api/auth/otp-verify` 输入动态码验证
5. 验证成功,`mfa_enabled` 置 1、`mfa_bound_at` 写入,直接签发 token
### 登录流程
1. 用户输入 user_id 和 name
2. 后端检查 `mfa_enabled`
3. 已绑定(`mfa_enabled=True`)且无 `otp_code` → 返回 `require_otp: True`
4. 前端显示 OTP 输入框
5. 用户输入 6 位动态码,再次登录
6. 后端验证通过,生成 token
### 解绑流程
1. 管理员 / 用户调用 `POST /api/auth/otp-unbind`
2. `mfa_secret` / `mfa_enabled` / `mfa_bound_at` 清空(`mfa_last_verified_at` 保留为审计记录)
### 管理员重置
1. 管理员调用 `POST /api/auth/otp-admin-reset/{employee_id}`
2. 目标员工的 `mfa_*` 字段清空,下次登录将重新引导绑定
## 错误码
| 错误码 | 说明 | 当前状态 |
|--------|------|----------|
| 1006 | OTP 验证码错误 | ✅ 仍在使用(`agents.py` 登录校验 `mfa_enabled=True` 分支) |
| 1007 | OTP 绑定失败 | ⚠️ 遗留数字码,原始 `otp` 设计;当前 `otp.py` 改用 `success_response` / `ErrorCode` 枚举(`E-prefix`),建议以代码为准复核 |
| 1008 | 请先绑定 OTP | ⚠️ 同上(当前由 `require_otp_bind: True` 表达,非错误码) |
| 1009 | OTP 验证失败 | ⚠️ 同上 |
| 1010 | OTP 解绑失败 | ⚠️ 同上 |
> 说明:当前错误码主体系为 `E{模块}{序号}` 枚举(见 `app/utils/error_codes.py`,如 `E1001` 认证失败)。上表数字码为 v0.5.6 遗留,仅 `1006` 在登录流程中仍被 `AppException(1006, ...)` 直接抛出;`1007``1010` 应结合 `otp.py` 实际返回复核。
---
**变更历史**
- 2026-08-05 修正端点命名与字段体系,对齐 AUTH-03 生产实现(`/api/auth/otp-*` + `mfa_*` 字段),移除与 `/api/mfa/*` 的过时对照