> **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 非目标 / 预留 - 微信扫码登录(仅保留禁用入口与接口占位,不开放功能) - 企业 SSO(OAuth2 / 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 API(MVP) | 端点 | 方法 | 说明 | |---|---|---| | `/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 | 仅占位 | 微信扫码回调换取系统 Token(v2 实现) | > 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` 中拦截。