Files
nexus/Project/fonrey/TECH_STACK/登录管理技术方案.md
2026-05-02 11:35:20 +08:00

357 lines
14 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.
> **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked.
# Fonrey 登录管理技术方案
**版本**: 4.1
**项目**: Fonrey 房产经纪管理系统
**技术栈**: Django 4.x + HTMX + Alpine.js + PostgreSQL 16 + Redis + Celery
**关联 PRD**: `PRD/登录管理/用户登录管理模块PRD.md`v3.0
**关联数据模型**: `DATA_MODEL/DATA_MODEL_LOGIN.md`(本方案不重复 DDL
**关联契约规范**: `TECH_STACK/API_CONTRACT.md`(全局 API 契约权威)
**关联测试规范**: `TECH_STACK/测试规范.md``TEST_CASES/TEST_CASES_LOGIN_MODULE.md`
**最后更新**: 2026-05-02
---
## 变更历史
| 日期 | 变更人 | 变更内容 |
| ---------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-04-30 | Atlas | 补充"变更历史"章节(文档治理) |
| 2026-05-02 | Sisyphus | 按 `ADR-20260502-003` 承接从 PRD v3.0 迁出的实现细节①§5.3 新增 Tenant Verify 请求/响应 JSON Schema②§5.3 新增预留 Wechat 端点 `GET /api/auth/wechat/qrcode/``POST /api/auth/wechat/callback/`仅占位MVP 不开放);③新增 §十三 Electron 客户端约定Tenant Code 存储/Session/登录页加载/多标签页/登出/窗口关闭/强制更新);④§六 关键流程约束补全密码错误锁定阈值、滑块容差、找回密码错误次数等数值口径,统一以本文件为准 |
## 一、文档定位与边界
本文件定义登录模块的实现口径:
1. 模块范围与职责边界
2. API 端点(页面 / HTMX / JSON
3. 安全策略(滑块验证、登录锁定、短信 OTP、会话
4. Redis/Celery 运行策略
5. 错误码与测试映射
> 本文件不展开数据表字段与索引。数据结构以 `DATA_MODEL_LOGIN.md` 为唯一权威。
---
## 二、范围定义(以 PRD v2.0 为准)
### 2.1 P0 必须覆盖
- Tenant Code 识别(首次启动 + 切换公司)
- 密码登录(手机号/密码 + 滑块)
- 首次登录强制修改密码
- 找回密码(纯短信三步流程)
- 手机验证码登录MVP 正式功能)
- 登录失败锁定 / 自动解锁 / 管理员解锁
- 安全登出与会话失效
### 2.2 非目标 / 预留
- 微信扫码登录(仅保留禁用入口与接口占位,不开放功能)
- 企业 SSOOAuth2 / SAML
- 风险评分/设备指纹
### 2.3 已废弃(不得实现)
- 找回用户名流程Story 4 已废弃)
---
## 三、模块架构边界
### 3.1 模块职责(`apps/account`
- 租户识别与租户上下文建立前置校验
- 登录鉴权、会话签发、登出销毁
- 首登改密门禁(`is_initial_password`
- 短信 OTP 发送/校验(找回密码 + 验证码登录)
- 登录与安全审计事件写入
### 3.2 多租户分层职责
| 层级 | Schema | 职责 |
|---|---|---|
| Tenant Code 校验 | Public | 校验 `tenant_code`、租户状态、品牌信息返回 |
| 登录认证 | Tenant | 账号鉴权、失败计数、锁定状态、会话签发 |
| 短信 OTP | Tenant | OTP 记录、场景区分、过期与尝试次数控制 |
### 3.3 外部依赖
| 依赖模块 | 用途 |
|---|---|
| `apps/org` | 员工状态联动(离职/停用不可登录) |
| `core/encryption.py` | 手机号加密/哈希能力 |
| `core/cache.py` | 滑块票据、频控、锁定计数、重置 token |
| `Celery` | 短信发送异步化(可选,建议) |
---
## 四、API 设计原则
1. 登录安全链路:**Tenant 校验 → 滑块验证 → 登录提交**。
2. 防枚举:账号不存在/停用等敏感状态采用统一外显文案。
3. 账号锁定是账号维度策略,不区分密码登录或验证码登录。
4. Redis 仅作运行态与频控,最终状态以数据库持久化字段为准。
5. 所有登录相关接口遵循 `TECH_STACK/API_CONTRACT.md` 的统一错误响应格式。
---
## 五、端点清单
### 5.1 页面路由SSR
| 路径 | 方法 | 鉴权 | 说明 |
| -------------------------------- | --- | --- | ------------------- |
| `/auth/tenant/identify/` | GET | 否 | Tenant 识别页 |
| `/auth/login/` | GET | 否 | 登录页(密码登录/验证码登录 Tab |
| `/auth/password/forgot/` | GET | 否 | 找回密码三步页 |
| `/auth/password/change-initial/` | GET | 是 | 首次登录强制改密页 |
> 说明:`/auth/wechat/*` 仅预留,不在 MVP 开放。
### 5.2 HTMX 片段端点
| 路径 | 方法 | 用途 | 返回 |
| ------------------------------ | --- | ----------- | ------- |
| `/auth/fragments/captcha/` | GET | 刷新滑块区块 | HTML 片段 |
| `/auth/fragments/login-form/` | GET | 登录 Tab 内容局刷 | HTML 片段 |
| `/auth/fragments/forgot-step/` | GET | 找回密码步骤局刷 | HTML 片段 |
### 5.3 JSON APIMVP
| 端点 | 方法 | 说明 |
|---|---|---|
| `/api/auth/tenant/verify/` | POST | Tenant Code 校验(公开接口) |
| `/api/auth/captcha/` | GET | 获取滑块拼图验证码(返回背景图 Base64 + 碎片图 Base64 + 验证 Token |
| `/api/auth/captcha/verify/` | POST | 校验滑块并签发 `captcha_pass_token` |
| `/api/auth/login/` | POST | 密码登录 |
| `/api/auth/login/phone/` | POST | 手机验证码登录 |
| `/api/auth/recover/password/request/` | POST | 找回密码步骤一:发 OTP |
| `/api/auth/recover/password/verify/` | POST | 找回密码步骤二:校验 OTP颁发 `sms_reset_token` |
| `/api/auth/recover/password/reset/` | POST | 找回密码步骤三:提交新密码 |
| `/api/auth/password/change-initial/` | POST | 首次登录强制改密提交 |
| `/api/auth/logout/` | POST | 登出销毁会话 |
#### 5.3.1 Tenant Verify 请求/响应 Schema
请求体:
```json
{ "tenant_code": "202500010001" }
```
成功响应HTTP 200
```json
{
"valid": true,
"tenant_name": "XX房产经纪有限公司",
"tenant_logo_url": "https://cdn.fonrey.com/tenants/xxx/logo.png",
"login_url": "https://xxx.fonrey.com/auth/login/"
}
```
失败响应HTTP 200业务态失败
```json
{
"valid": false,
"error_code": "AUTH_TENANT_NOT_FOUND",
"message": "识别码无效"
}
```
限流超限响应HTTP 429遵循 `API_CONTRACT.md` 统一错误格式,`code = AUTH_TENANT_RATE_LIMITED`
### 5.4 预留端点MVP 不开放)
| 端点 | 方法 | 状态 | 说明 |
|---|---|---|---|
| `/api/auth/wechat/qrcode/` | GET | 仅占位 | 微信扫码登录二维码获取v2 实现) |
| `/api/auth/wechat/callback/` | POST | 仅占位 | 微信扫码回调换取系统 Tokenv2 实现) |
> MVP 内 URLConf 中**不注册**这两个路由,登录页 UI 入口以禁用态展示v2 启用时再按本表落地路由与视图。
---
## 六、关键流程约束
### 6.1 Tenant 识别
- `tenant_code` 固定 12 位数字,前后空格自动 trim
- 成功返回租户名称、Logo URL、登录地址
- 失败返回:`valid=false` + 统一错误信息
- 接口公开但必须限流:单 IP 每分钟 ≤ 10 次
### 6.2 密码登录
请求体(示例):
```json
{
"phone": "13800138000",
"password": "***",
"captcha_pass_token": "token"
}
```
关键规则:
- 滑块通过后方可提交登录
- 密码连续错误 ≥ 5 次,锁定 30 分钟
- `is_initial_password = true` 时强制跳转改密页
- 错误文案统一,不泄露账号存在性细节
### 6.3 找回密码(纯短信三步)
#### 步骤一:发送 OTP
- `scene = password_reset`
- OTP 有效期10 分钟
- 同手机号频控5 次/小时
- 手机号不存在/停用:统一提示“如该手机号已注册,验证码将在 1 分钟内发送”
#### 步骤二:校验 OTP
- 正确且未过期:签发 `sms_reset_token`15 分钟,一次性)
- 错误累计尝试≥5 次作废该 OTP
- 过期:提示重新获取
#### 步骤三:重置密码
- 必须携带有效 `sms_reset_token`
- 成功后:`is_initial_password = false`
- 该用户所有会话立即失效,跳回登录页
### 6.4 手机验证码登录MVP 正式)
请求体(示例):
```json
{
"phone": "13800138000",
"sms_code": "123456"
}
```
关键规则:
- 获取登录验证码前必须先通过滑块
- `scene = login`
- OTP 有效期5 分钟
- 同手机号频控10 次/小时(与 `password_reset` 独立计数)
- OTP 错误 ≥5 次作废
- 账号 `locked``disabled` 时,验证码登录同样拒绝
---
## 七、Redis Key 规范(对齐 PRD
| Key | TTL | 说明 |
|---|---|---|
| `captcha_token:{uuid}` | 3 分钟 | 滑块验证会话 |
| `captcha_pass:{uuid}` | 3 分钟 | 一次性通过凭证 |
| `login_fail:{tenant_id}:{username}` | 30 分钟 | 登录失败计数 |
| `sms_limit:password_reset:{phone}` | 1 小时 | 找回密码 OTP 发送频控≤5次 |
| `sms_limit:login:{phone}` | 1 小时 | 登录 OTP 发送频控≤10次 |
| `sms_reset_token:{token}` | 15 分钟 | 找回密码步骤三凭证 |
| `tenant_verify_ip:{ip}` | 1 分钟 | Tenant 校验接口 IP 限流≤10次 |
---
## 八、安全与合规
1. 密码仅允许 Django 安全哈希PBKDF2/Argon2
2. OTP 明文不得入库,仅存哈希。
3. 敏感字段日志脱敏手机号、token、验证码
4. 会话过期或登出后,受保护页面必须重定向登录。
5. HTTPS 强制,不允许明文传输降级。
---
## 九、错误码建议(登录模块)
| code | HTTP | 中文含义 |
|---|---|---|
| `AUTH_TENANT_NOT_FOUND` | 400 | 租户识别码无效 |
| `AUTH_TENANT_RATE_LIMITED` | 429 | Tenant 校验请求过于频繁 |
| `AUTH_CAPTCHA_INVALID` | 400 | 滑块验证失败 |
| `AUTH_INVALID_CREDENTIAL` | 401 | 手机号或密码错误 |
| `AUTH_ACCOUNT_LOCKED` | 423 | 账号已锁定 |
| `AUTH_ACCOUNT_DISABLED` | 403 | 账号已停用 |
| `AUTH_SMS_OTP_INVALID` | 400 | 短信验证码错误 |
| `AUTH_SMS_OTP_EXPIRED` | 400 | 短信验证码过期 |
| `AUTH_SMS_OTP_RATE_LIMITED` | 429 | OTP 发送超限 |
| `AUTH_SMS_RESET_TOKEN_INVALID` | 400 | 重置凭证无效或过期 |
| `AUTH_PASSWORD_WEAK` | 400 | 新密码不满足强度规则 |
---
## 十、测试映射(与全局编号对齐)
- 测试编号规范:`TEST_CASES/TEST_CASE_ID_SPEC.md`
- 注册表:`TEST_CASES/TEST_CASE_REGISTRY.md`
- 登录模块用例:`TEST_CASES/TEST_CASES_LOGIN_MODULE.md`
建议最小执行集:
- Tenant 识别:`TC-FON-000001` ~ `000010`
- 密码登录与锁定:`TC-FON-000011` ~ `000028`
- 首登改密:`TC-FON-000029` ~ `000033`
- 找回密码:`TC-FON-000034` ~ `000044`
- 验证码登录:`TC-FON-000045` ~ `000048`
---
## 十一、落地顺序建议
1. Tenant 识别 + 密码登录主链路
2. 滑块与失败锁定
3. 首登改密门禁
4. 找回密码三步(短信)
5. 手机验证码登录scene=login
6. 报表与监控补齐
---
## 十二、文档同步规则
- PRD 登录模块变更:同步本文件
- 数据结构调整:同步 `DATA_MODEL_LOGIN.md`
- 测试用例新增/变更:同步 `TEST_CASES/TEST_CASE_REGISTRY.md` 与登录用例文档
- API 契约调整:同步 `TECH_STACK/API_CONTRACT.md`
---
## 十三、Electron 客户端约定(实现口径)
> 承接自原 PRD §5.7(按 `ADR-20260502-003` 迁入本文件)。本节为 Electron 渲染层与主进程在登录链路上的实现规约,开发时必须严格遵循。
### 13.1 存储与会话
| 约定项 | 实现规格 |
|---|---|
| Tenant Code 存储 | `electron-store``app.getPath('userData')` 下的配置文件,**必须 AES 加密**,禁止明文落盘 |
| Session Token 存储 | 内存(主进程 `global` 变量)+ Chromium 管理的 `session` Cookie`HttpOnly` + `Secure` + `SameSite=Strict`**禁止写入磁盘明文文件** |
| 多标签页 | 同一 `BrowserWindow` 内,所有页面共享同一 `session.defaultSession`,复用同一 Session Cookie |
| 窗口关闭 | Session 保留(不自动登出);下次启动客户端时,若 Cookie 未过期且 `/api/auth/whoami/` 校验通过,直接进入系统 |
### 13.2 登录页加载流程
1. 主进程读取本地缓存的 `tenant_code`
- 不存在 → `BrowserWindow.loadURL('app://tenant-identify')`(本地静态页或专用 Tenant 识别 URL
- 存在 → 调用 `POST /api/auth/tenant/verify/` 复核(防止租户被禁用 / 改名)
- 成功 → 构建 `https://{tenant_slug}.fonrey.com/auth/login/`,通过 `BrowserWindow.loadURL()` 加载
- 失败 → 清除本地 `tenant_code` 缓存 → 跳回 Tenant 识别页
2. 切换公司:用户在登录页点击「切换公司」 → 主进程清缓存 → 关闭当前 `BrowserWindow` → 重新走步骤 1。
### 13.3 登出与强制更新
| 场景 | 客户端动作 |
|---|---|
| 用户登出 | 调用 `POST /api/auth/logout/` → 清除 Chromium Session Cookie`session.defaultSession.clearStorageData({ storages: ['cookies'] })`)→ 跳转登录页 |
| 服务端 Session 过期401 | 渲染层捕获 401 → 主进程清 Cookie → 跳转登录页并提示"登录已过期,请重新登录" |
| 客户端版本低于 `min_required_version` | 加载登录页前先展示"请更新客户端"模态,阻断登录流程;联动 `平台管理后台技术方案.md` 客户端发布章节 |
### 13.4 安全约束(与 AGENTS.md §5 对齐)
- 渲染进程必须 `nodeIntegration: false``contextIsolation: true``sandbox: true`
- 渲染层禁止内嵌业务逻辑或本地数据库(壳应用原则);
- 主进程与渲染层之间通过 `contextBridge` 暴露的最小白名单 IPC 接口通信;
- 仅允许加载 `https://*.fonrey.com``app://` 协议,其他 URL 在 `will-navigate` 中拦截。