系统架构设计 + 任务分解 — 三端认证重构(增量)
文档版本:v1.0(架构师交付稿)
架构师:高见远 (software-architect)
依据:增量 PRD docs/02-产品需求/04-增量PRD-三端认证重构.md + 已锁定决策(决策1–7)
原则:基于现有代码的最小变更增量重构,不引入新框架
Part A:系统设计
1. 实现方案 + 框架选型
1.1 技术栈(沿用,不新增框架)
- 后端:FastAPI + SQLAlchemy(async) + Redis + Pydantic。认证分层沿用
api(路由) / services(逻辑) / models(数据) 结构。
- 前端:三端独立 Vue3 + Vite SPA(
frontend-h5 / frontend-agent / frontend-admin),各自 Axios 实例 + 响应拦截器。
- OTP 算法:
pyotp(已安装于 backend/venv,版本 2.10.0)、qrcode 已存在。无需新增任何依赖,直接复用 backend/app/services/mfa_service.py 的 MFAService 封装(TOTP secret 生成 / 校验 / 二维码 / Redis 标记)。
1.2 本次增量改动点(对照 PRD)
| 决策 |
改动点 |
类型 |
| 1 三端独立入口 |
移除 /itportal/ 与 /api/portal/*;清理 portal_token 引用 |
删除 |
| 2 员工端唯一认证 |
H5 仅 snsapi_base 静默授权 → 直进;Token 经 ?token= 镜像 h5_token;非 wxwork UA 跳拦截页(仅生产) |
改 |
| 3 坐席/管理两方式并列 |
移除「企微已登录+角色→免密直接进入」分支;保留 ①扫码 ②账密+OTP |
删/改 |
| 4 OTP 输入框延迟渲染 |
账密验证通过后才渲染 OTP(前端修正,原型已对齐) |
改 |
| 5 OTP 接口统一 |
新增 /api/auth/otp-*,作废 /api/mfa/* 与 /api/agents/otp-* |
新增/删 |
| 6 管理端 IP 白名单 |
新增后端中间件,仅生产启用,非白名单 403 |
新增 |
| 7 响应契约方案A |
三端拦截器统一:成功返回内层 data,失败抛 {code,message} |
改 |
| 8 env-gating |
UA 校验 / IP 白名单 / 真实企微 OAuth 仅生产启用;本地走 dev/mock |
新增 |
1.3 关键设计决策(基于代码核实)
- OTP 逻辑复用:既有
backend/app/api/mfa.py 用 MFAService(干净);而 backend/app/api/agents.py 的 /agents/otp-* 用内联 pyotp 重复实现。新端点统一落到 新增 backend/app/api/otp.py(前缀 /auth),复用 MFAService 与 agents.mfa_* 字段。Redis 复用标记 key 维持 mfa:verified:{employee_id}(TTL 1800s),dependencies.require_high_risk_otp 无需改动。
- 员工
?token= 镜像已具备:frontend-h5/src/router/index.ts 已读取 URL ?token= → 写 localStorage.h5_token(Bug#4 修复)。本次仅把后端 OAuth 回调从「code→前端 POST 取 token」改为「code→后端 302 /itdesk/?token=XXX」,复用既有镜像逻辑,更贴合决策2。
- 管理端 IP 白名单:以 FastAPI 中间件实现,仅对
/api/admin/* 与 /api/auth/otp-admin-* 生效(生产环境 APP_ENV 判定),非白名单返回 {code:4004, message:"无访问权限"},前端据此展示无权限拦截页。
- 免密分支移除:
agents.py agent_login 中 wecom_verified + role → skip_otp=True 整段删除;前端 Login.vue 移除「企微免密登录 / JS-SDK 快捷登录 / 智能检测自动跳转」分支。
2. 文件列表(标注【新增】/【修改】/【删除】)
2.1 后端
| 路径 |
操作 |
说明 |
backend/app/config.py |
【修改】 |
新增 app_env(默认 "dev")、admin_allowed_ips(默认 "117.147.35.138,218.75.34.87,10.240.0.0/16");复用 wecom_corp_id |
backend/app/utils/env_gating.py |
【新增】 |
is_production()(按 app_env)、ip_in_whitelist(client_ip, allowed)(支持 CIDR);统一 env-gating 判定 |
backend/app/middleware/admin_ip_whitelist.py |
【新增】 |
AdminIPWhitelistMiddleware:仅 app_env==production 且路径命中 admin 前缀时校验客户端 IP |
backend/app/main.py |
【修改】 |
注册 AdminIPWhitelistMiddleware(在 CORS 之后、路由之前) |
backend/app/api/otp.py |
【新增】 |
统一 OTP 路由(前缀 /auth):otp-status / otp-bind / otp-verify / otp-unbind / otp-admin-reset/{id} / otp-admin-users |
backend/app/api/router.py |
【修改】 |
注册 otp_router;注销 portal_router / mfa_router / agents 内 otp 端点 |
backend/app/api/mfa.py |
【删除】 |
作废 /api/mfa/* 与 /api/admin/mfa/* |
backend/app/api/agents.py |
【修改】 |
删除 /agents/otp-bind / /agents/otp-verify / /agents/otp-unbind 三端点;agent_login 移除 skip_otp 免密分支 |
backend/app/api/h5.py |
【修改】 |
修 get_oauth_authorize_url 的 redirect_uri 由 /itportal/ → /itdesk/(现网bug);新增 GET /h5/oauth/sns-callback 302 带 ?token=;_require_wework_ua 改用 env_gating.is_production() |
backend/app/api/auth_wecom_sso.py |
【修改】 |
sso_verify 用 success_response 包裹(一致性);备注 env-gating |
backend/app/api/dev_auth.py |
【修改】 |
三端点用 success_response 包裹(CTRT-P1-1 一致性,dev 仅本地) |
backend/app/api/portal.py |
【删除】 |
作废 /api/portal/* |
backend/app/dependencies.py |
【修改】 |
文档更新(mfa:verified: key 不变,仅端点路径变) |
2.2 前端 H5(frontend-h5)
| 路径 |
操作 |
说明 |
src/api/index.ts |
【修改】 |
拦截器统一方案A(成功返回内层 data);移除非 wxwork→/login 的 dev 兜底,生产改 /wework-only;清理 X-Employee-Id 遗留头 |
src/router/index.ts |
【修改】 |
生产非 wxwork UA → 跳转 /wework-only(沿用现有 ?token= 镜像分支不动) |
src/api/employee.ts |
【修改】 |
调用点适配内层 data(response.data → response) |
src/api/conversation.ts 等全部 src/api/*.ts |
【修改】 |
CTRT-P0-2 全量回归(约 8 个文件) |
src/views/WeworkOnly.vue |
【复用】 |
非企微拦截页(已存在,无需新建) |
2.3 前端坐席(frontend-agent)
| 路径 |
操作 |
说明 |
src/api/index.ts |
【修改】 |
拦截器统一方案A;移除 portal_token 清理遗留 |
src/views/Login.vue |
【修改】 |
移除「企微免密登录 / JS-SDK 快捷登录 / 智能检测三选项 / onMounted 自动 sso 跳转」;保留 ①扫码 ②账密+OTP(OTP 已 v-if="requireOtp" 延迟渲染) |
src/api/mfa.ts |
【修改】 |
路径改 /api/auth/otp-*;调用点适配内层 data |
src/api/*.ts(qrcode.ts、conversation.ts、message.ts 等) |
【修改】 |
CTRT-P0-2 全量回归 |
2.4 前端管理(frontend-admin)
| 路径 |
操作 |
说明 |
src/api/index.ts |
【修改】 |
拦截器统一方案A(与坐席同形态;admin 仍无静默刷新,401/1002 清 admin_token 跳 /login) |
src/views/Login.vue |
【修改】 |
移除「企微免密登录」按钮与 JS-SDK 检测分支;保留 ①扫码 ②账密+OTP;非白名单 403 → 无权限页 |
src/views/NoPermission.vue |
【新增】 |
管理端「无访问权限」拦截页(对应 PRD 原型 note 4) |
src/api/mfa.ts |
【修改】 |
/admin/mfa/users → /api/auth/otp-admin-users;/admin/mfa/reset/{id} → /api/auth/otp-admin-reset/{id};适配内层 data |
src/api/*.ts |
【修改】 |
CTRT-P0-2 全量回归 |
2.5 待清理工程
| 路径 |
操作 |
说明 |
frontend-portal/(整个工程) |
【删除】 |
统一入口 Portal 前端(决策1/C8),含 QrcodeLogin.vue / PortalSelect.vue / stores/portal.ts 等 |
3. 数据结构和接口
3.1 类图(Mermaid)
3.2 OTP 接口契约(前缀 /api/auth,全部走统一信封)
| 方法 & 路径 |
鉴权 |
请求体 |
成功响应 data |
说明 |
GET /otp-status |
登录用户 |
— |
{bound, enabled, last_verified_at} |
路由守卫用 |
POST /otp-bind |
登录用户 |
— |
{secret, otpauth_url, qr_code_base64} |
生成 secret 存 mfa_secret(enabled=False);已 enabled 拒绝 |
POST /otp-verify |
登录用户 |
{otp_code} |
{verified, bound, expires_in} |
未启用→确认绑定(set enabled+bound_at);写 mfa:verified:;用于 bind 闭环 + 高危操作 |
POST /otp-unbind |
登录用户 |
{otp_code} |
{success} |
校验后清空 mfa_secret/enabled |
POST /otp-admin-reset/{employee_id} |
admin 角色 |
— |
{success} |
丢手机兜底,无 OTP 直接清空 |
GET /otp-admin-users |
admin 角色 |
?keyword&bound&page&page_size |
{items,total,page,page_size} |
管理页用户 MFA 列表(保留管理页) |
3.3 登录接口契约
| 方法 & 路径 |
请求体 |
响应 data |
POST /api/agents/login |
{user_id, name?, password, otp_code?} |
成功:{token, user_id, name, role, ...};mfa_enabled 且无 otp_code:{require_otp:true, user_id, name, role}(不带 token) |
POST /api/auth_qrcode/create |
— |
{ticket, qrcode_url, qrcode_png_base64, expires_in, expires_at} |
GET /api/auth_qrcode/poll/{ticket} |
— |
{status, employee_id, name, token}(status: waiting/scanned/confirmed/expired) |
POST /api/auth_qrcode/confirm |
{ticket, otp_code?} |
{token, employee_id, name, roles, require_otp} |
GET /api/h5/oauth/authorize |
?redirect_uri |
{authorize_url}(prod 强制 wxwork UA,否则 4003) |
GET /api/h5/oauth/sns-callback |
?code |
302 → /itdesk/?token=XXX |
POST /api/h5/oauth/callback |
{code} |
{token, employee_id, ...}(dev 兜底,保留) |
GET /api/dev/login |
?userid&role=user |
{token, user}(dev/mock,DEV_MODE 启用) |
POST /api/auth/refresh |
?token |
{token, expires_in}(坐席静默刷新;H5/admin 不刷新) |
管理端登录复用 POST /api/agents/login(同契约,role=admin)。
4. 程序调用流程(Mermaid 时序图)
4.1 员工端 OAuth 静默授权(snsapi_base → ?token= 镜像)
4.2 坐席/管理 扫码登录(auth_qrcode)
4.3 坐席/管理 账号密码 + OTP
4.4 令牌过期 / 401 处理
5. 任务列表(有序、含依赖、按 AUTH-/CTRT- 分组)
说明:本重构涉及「后端 + 三前端」,按 PRD 交付要求拆为可独立执行的细粒度任务,按 AUTH(认证)/ CTRT(契约)分组,标注依赖与可并行项。前端契约任务(CTRT)与后端任务可并行启动。
5.1 认证类(AUTH-)
| ID |
任务 |
涉及文件 |
依赖 |
可并行 |
验收标准 |
| AUTH-01 |
环境门控与配置 |
backend/app/config.py【改】、backend/app/utils/env_gating.py【新】 |
无 |
是(与 CTRT-01 并行) |
is_production()、ip_in_whitelist() 单测通过;app_env/admin_allowed_ips 可由环境变量注入 |
| AUTH-02 |
管理端 IP 白名单中间件 |
backend/app/middleware/admin_ip_whitelist.py【新】、backend/app/main.py【改】 |
AUTH-01 |
否 |
仅 prod + /api/admin/* 与 /api/auth/otp-admin-* 命中;非白名单返回 {code:4004};dev 关闭不拦截 |
| AUTH-03 |
统一 OTP 路由(复用 MFAService) |
backend/app/api/otp.py【新】 |
AUTH-01 |
是(与 AUTH-05 并行) |
6 端点齐备;复用 mfa:verified: key;otp-bind→otp-verify 闭环、admin-reset 生效 |
| AUTH-04 |
路由收口与旧端点清理 |
backend/app/api/router.py【改】、backend/app/api/mfa.py【删】、backend/app/api/agents.py【改】 |
AUTH-03 |
否 |
router 注册 otp、注销 portal/mfa/agents-otp;/agents/otp-* 与 /api/mfa/* 不可达;agent_login 无 skip_otp 免密分支 |
| AUTH-05 |
H5 OAuth 修 bug + ?token= 重定向 |
backend/app/api/h5.py【改】 |
AUTH-01 |
是(与 AUTH-03 并行) |
authorize redirect_uri=/itdesk/;sns-callback 302 带 ?token=;wxwork UA 校验仅 prod |
| AUTH-06 |
移除 Portal 工程与 portal_token |
backend/app/api/portal.py【删】、frontend-portal/【删】、frontend-agent/src/api/index.ts【改】 |
AUTH-04 |
否 |
/api/portal/* 不可达;frontend-portal 已删;三端无 portal_token 引用 |
| AUTH-07 |
后端信封一致性审计 |
backend/app/api/dev_auth.py【改】、backend/app/api/auth_wecom_sso.py【改】、其余路由抽查 |
无 |
是(并行) |
全路由经 success_response/AppException;dev/sso 端点用信封包裹;无裸 dict 直返 |
| AUTH-08 |
H5 员工端登录页/拦截 |
frontend-h5/src/router/index.ts【改】、frontend-h5/src/views/WeworkOnly.vue【复用】 |
CTRT-01 |
否 |
prod 非 wxwork → /wework-only;?token= 镜像保留;mock 走 /login |
| AUTH-09 |
坐席登录页清理 |
frontend-agent/src/views/Login.vue【改】 |
CTRT-01 |
否 |
仅 ①扫码 ②账密+OTP;无免密/JS-SDK 分支;无 onMounted 自动 sso 跳转;OTP 仍延迟渲染 |
| AUTH-10 |
管理登录页清理 + IP 拦截页 |
frontend-admin/src/views/Login.vue【改】、frontend-admin/src/views/NoPermission.vue【新】 |
AUTH-02, CTRT-01 |
否 |
移除免密按钮;非白名单 403 → 无权限页;扫码+账密+OTP 保留 |
| AUTH-11 |
前端 OTP API 模块迁移 |
frontend-agent/src/api/mfa.ts【改】、frontend-admin/src/api/mfa.ts【改】 |
AUTH-03, CTRT-02 |
否 |
路径改 /api/auth/otp-*;调用点适配内层 data;admin 列表/重置端点对齐 |
5.2 契约类(CTRT-)
| ID |
任务 |
涉及文件 |
依赖 |
可并行 |
验收标准 |
| CTRT-01 |
三端拦截器统一方案A(响应+请求) |
frontend-h5/src/api/index.ts【改】、frontend-agent/src/api/index.ts【改】、frontend-admin/src/api/index.ts【改】 |
无 |
是(与 AUTH-01/03/05/07 并行) |
成功返回内层 data;失败抛 {code,message};请求统一 Authorization:Bearer;清 X-Employee-Id/portal_token 遗留 |
| CTRT-02 |
三端调用点全量回归 |
三端 src/api/*.ts(employee/conversation/mfa/qrcode/message 等约 15 文件) |
CTRT-01 |
否 |
H5 response.data→response;agent/admin response.data.data→response;编译+核心链路无字段错取 |
| CTRT-03 |
401/1002 上报形态统一 |
三端 src/api/index.ts(含 CTRT-01) |
无 |
是 |
三端 catch 到统一 {code,message};各自重授权逻辑符合 PRD §3「401 处理约定」表 |
5.3 联调与验证
| ID |
任务 |
涉及文件 |
依赖 |
可并行 |
验收标准 |
| VERIFY-01 |
env-gating 与本地 dev/mock 联调 |
全端 |
全部 AUTH/CTRT |
否 |
本地 VITE_WECOM_CORP_ID 空 → mock 登录链路通;真实 OAuth 仅 staging/prod |
| VERIFY-02 |
三端认证回归测试 |
— |
VERIFY-01 |
否 |
满足 PRD §5.3 五条自动化断言(dev/mock 返回 code:0+合法 token;失效 token 触发各端重授权;拦截器统一契约;OTP 闭环) |
6. 依赖包列表
- 后端:无新增。
pyotp(2.10.0)、qrcode、redis、bcrypt、passlib、slowapi 均已存在。
- 前端:无新增。
axios、vue-router、pinia、element-plus(agent/admin)、vant(h5) 均已存在。
7. 共享知识(跨文件约定)
- Token 键名
- localStorage:
h5_token / agent_token / admin_token。
- Redis:
employee:token:{token}→employee_id(H5)、user:token:{token}→JSON(坐席/管理统一格式)、agent:token:{token}→user_id(旧格式兼容)。
- 请求头统一
Authorization: Bearer <token>;移除 X-Employee-Id 明文头与 portal_token。
- 拦截器返回形态(方案A):成功返回内层
data;失败 reject 标准化错误对象 {code, message}。三端一致。
- 环境变量
- 后端:
APP_ENV(production/staging/dev/test,默认 dev)、WECOM_CORP_ID、ADMIN_ALLOWED_IPS("117.147.35.138,218.75.34.87,10.240.0.0/16")、DEV_MODE、MOCK_LOGIN_ENABLED、WECOM_SSO_ENABLED。
- 前端:
VITE_WECOM_CORP_ID(空 = dev/mock)、VITE_APP_ENV(可选,区分 prod/dev)。
- env-gating 矩阵(仅生产启用):UA 校验 / IP 白名单 / 真实企微 OAuth;本地/测试跳过,走 dev/mock。
- 401 / 1002 处理约定
- H5:清
h5_token;prod→重走 OAuth(带防循环计数,上限 3);mock→跳 /itdesk/login。
- 坐席:先静默
POST /api/auth/refresh;失败清 agent_token 跳 /login(不再清 portal_token)。
- 管理:清
admin_token 跳 /login。
- OTP Redis 标记:key
mfa:verified:{employee_id},TTL 1800s,由 MFAService 读写;require_high_risk_otp 依赖此 key(端点改名不影响)。
- 三端入口:
/itdesk/(H5) / /itagent/(坐席) / /itadmin/(管理);移除 /itportal/。
- 错误码:
1002=未授权;4003=非企微环境/无权限;4004=管理端 IP 无权限(新增);1006=OTP 验证码错误。
8. 待明确事项
10.240.0.0 网段掩码:PRD 写「10.240.0.0(内网/VPN 网段)」,本设计按 /16 CIDR 处理,请确认精确掩码(如 /12 / /16)。
- 管理端 MFA 用户列表端点:
/admin/mfa/users 是否随 PRD 作废?本设计默认迁移为 GET /api/auth/otp-admin-users 以保留管理页功能;若产品决定下线该管理页,则可一并删除。
agents/login 内联 pyotp 校验:本次保持最小变更(不重构为复用 MFAService);如需消除重复实现,列为可选优化(不影响功能)。
- 员工端 OAuth 跳转方式:本设计采用「后端
sns-callback 302 带 ?token=」(贴合决策2,复用现有 ?token= 镜像);旧的「前端 code→POST /h5/oauth/callback 取 token」路径保留为 dev 兜底。如坚持完全走 ?token=,可删除旧 POST 回调。
wecom_jsdk_login 接口:决策4 移除前端「免密」入口,但后端 /api/auth_wecom/jsdk-login 接口本设计保留(仅前端不再调用),避免影响其他潜在调用方;如需彻底删除请确认。
附:类图见 docs/class-diagram.mermaid,时序图见 docs/sequence-diagram.mermaid。