14 KiB
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/登录页加载/多标签页/登出/窗口关闭/强制更新);④§六 关键流程约束补全密码错误锁定阈值、滑块容差、找回密码错误次数等数值口径,统一以本文件为准 |
一、文档定位与边界
本文件定义登录模块的实现口径:
- 模块范围与职责边界
- API 端点(页面 / HTMX / JSON)
- 安全策略(滑块验证、登录锁定、短信 OTP、会话)
- Redis/Celery 运行策略
- 错误码与测试映射
本文件不展开数据表字段与索引。数据结构以
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 设计原则
- 登录安全链路:Tenant 校验 → 滑块验证 → 登录提交。
- 防枚举:账号不存在/停用等敏感状态采用统一外显文案。
- 账号锁定是账号维度策略,不区分密码登录或验证码登录。
- Redis 仅作运行态与频控,最终状态以数据库持久化字段为准。
- 所有登录相关接口遵循
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
请求体:
{ "tenant_code": "202500010001" }
成功响应(HTTP 200):
{
"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,业务态失败):
{
"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 密码登录
请求体(示例):
{
"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 正式)
请求体(示例):
{
"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次) |
八、安全与合规
- 密码仅允许 Django 安全哈希(PBKDF2/Argon2)。
- OTP 明文不得入库,仅存哈希。
- 敏感字段日志脱敏(手机号、token、验证码)。
- 会话过期或登出后,受保护页面必须重定向登录。
- 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
十一、落地顺序建议
- Tenant 识别 + 密码登录主链路
- 滑块与失败锁定
- 首登改密门禁
- 找回密码三步(短信)
- 手机验证码登录(scene=login)
- 报表与监控补齐
十二、文档同步规则
- 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 登录页加载流程
- 主进程读取本地缓存的
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 识别页
- 成功 → 构建
- 不存在 →
- 切换公司:用户在登录页点击「切换公司」 → 主进程清缓存 → 关闭当前
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中拦截。