Files
nexus/Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md
2026-04-30 18:40:55 +08:00

671 lines
44 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.
# PRD用户登录管理模块
**状态**: Draft
**作者**: 产品经理
**最后更新**: 2026-04-30v2.0 根据 review 后的 §4 用户故事全面同步 §5 功能详细说明:删除找回用户名流程及邮件模板;找回密码改为纯短信流程;新增 §5.5 手机验证码登录详细说明§6 技术注意事项更新短信依赖/风险/开放问题§8.2 接口清单同步正式功能状态)
**版本**: 2.0
**所属系统**: Fonrey 房产经纪管理系统
**关联模块**: 组织人事管理、权限管理、系统管理
---
## 1. 问题陈述
### 1.1 背景
Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用多租户架构(`django-tenants` + PostgreSQL Schema 隔离)。终端用户通过 **Windows 桌面客户端Electron** 使用系统,无需手动输入网址即可打开 Web 应用。
在多租户环境下,用户的身份验证流程比单租户系统更复杂:
- 用户安装客户端后,系统必须先识别当前设备归属哪个租户,才能加载对应租户的登录界面和数据隔离环境
- 每家经纪公司作为独立租户,其员工账号、组织结构、数据均完全隔离
- 经纪人账号须与实名员工档案绑定,确保每一条操作记录可追溯至具体自然人
- 现阶段登录方式以账号密码为主;手机验证码登录、微信扫码登录需预留接口,待移动端(小程序)上线后实现
### 1.2 核心痛点
| 痛点 | 影响方 | 当前代价 |
|------|--------|---------|
| 多租户环境下,客户端不知道应该连接哪个租户的服务端 | 新用户首次安装后无法正常使用 | 系统无法启动,用户体验极差 |
| 账号密码裸露登录,缺乏验证码保护 | 所有用户 | 存在暴力破解、自动化恶意登录风险 |
| 用户忘记账号或密码无自助找回通道 | Agent经纪人 | 依赖管理员手动重置,效率低 |
| 账号未与实名经纪人档案绑定 | Tenant Admin租户管理员、合规审计 | 操作行为无法追溯至自然人 |
| 无多因素认证,安全系数低 | 管理层、数据合规 | 存在账号冒用、数据泄露风险 |
### 1.3 目标用户
| 角色 | 描述 | 使用频率 |
|------|------|----------|
| Agent经纪人 | 每日登录系统使用房源/客源功能 | 每日高频 |
| 店长 / 经理 | 登录后查看全店数据、管理任务 | 每日 |
| Tenant Admin租户管理员 | 管理账号创建、密码重置、租户初始化 | 按需 |
| 新安装用户 | 首次安装客户端后需完成 Tenant 识别 | 一次性 |
---
## 2. 目标与成功指标
| 目标 | 指标 | 当前基准 | 目标值 | 衡量周期 |
|------|------|----------|--------|----------|
| 降低登录失败率 | 账号密码正确情况下的登录成功率 | 待统计 | ≥ 99% | 上线后 30 天 |
| 防止恶意登录 | 每日验证码拦截异常登录请求数 | 0无保护 | 建立基线同IP异常次数 > 5次/分钟触发封锁 | 上线后持续监控 |
| 提升找回账号效率 | 用户自助找回密码耗时 | 依赖管理员约1工作日 | < 3 分钟(短信验证码自助) | 上线后 30 天 |
| 确保账号实名绑定率 | 拥有系统账号且未与员工档案绑定的账号比例 | 待统计 | 0%(强制绑定) | 上线即达标 |
| Tenant 识别成功率 | 首次安装后成功完成 Tenant 识别的用户比例 | 待统计 | ≥ 98% | 上线后 30 天 |
---
## 3. 非目标(本期不做)
- **手机验证码登录**:移动端小程序上线后实现,本期**接口预留**UI 入口以「即将开放」禁用态展示
> **注意**:本条已更新。手机验证码登录已升级为 **MVP 正式功能**(见 Story 5与密码登录并列提供。此非目标条目保留仅作版本记录已失效。
- **微信扫码登录**:移动端小程序上线后实现,本期**接口预留**UI 入口以「即将开放」禁用态展示
- **单点登录SSO/ 企业微信集成**:后续版本规划
- **多设备并发登录的强制踢出策略**:本期允许同账号多端登录,后续安全策略模块规划
- **登录时段限制 / IP 白名单**:安全策略模块另行规划
- **管理后台Platform Admin登录**Tenant Admin租户管理员登录管理后台的流程属于系统管理模块本 PRD 专注租户内用户登录
---
## 4. 用户故事与验收标准
---
### Story 1新用户首次启动客户端——Tenant 识别
**As** 新安装 Fonrey 客户端的经纪人,**I want** 在首次启动时输入所属公司的 12位 Tenant Code 完成租户识别,**So that** 客户端能连接到正确的服务端,后续显示对应公司的登录界面和数据。
**验收标准**
- [ ] 客户端首次启动时(本地无 Tenant Code 缓存自动呈现「Tenant 识别」界面,而非直接显示登录界面
- [ ] 界面包含:产品 Logo、产品名称「Fonrey 房睿」、说明文案「请输入您公司的专属识别码」、Tenant Code 输入框、「确认」按钮
- [ ] Tenant Code 输入框支持粘贴操作,自动去除前后空格
- [ ] 点击「确认」后,客户端向服务端发起 Tenant 验证请求(`POST /api/auth/tenant/verify/`展示加载状态spinner
- [ ] **验证成功**服务端返回租户名称及品牌信息如公司名称、Logo URL客户端将 Tenant Code 写入本地持久化存储自动跳转至该租户的登录界面界面顶部展示「正在登录XX 房产」
- [ ] **验证失败Tenant Code 无效)**输入框下方显示红色错误提示「识别码无效请联系您的Tenant Admin租户管理员获取正确的识别码」Tenant Code 不写入本地缓存;用户可重新输入
- [ ] **网络异常**:显示「网络连接失败,请检查网络后重试」,提供「重试」按钮
- [ ] 非首次启动(本地已有合法 Tenant Code 缓存):直接跳过识别界面,进入登录界面
- [ ] 登录界面提供「切换公司」入口(链接文字,非主要 CTA点击后清除本地 Tenant Code 缓存并重新显示 Tenant 识别界面;确认前弹出二次确认「切换公司将退出当前账号,是否继续?」
- [ ] Tenant 验证接口属于公开接口,无需鉴权;但需对单 IP 请求频率限制(每分钟 ≤ 10 次)以防止枚举攻击
---
### Story 2经纪人通过手机号和密码登录
**As** 已识别租户的经纪人,**I want** 通过手机号和密码完成登录,**So that** 进入系统开始工作。
> **说明**普通员工的登录账号即为其手机号由Tenant Admin租户管理员在新增员工时自动创建无需记忆额外用户名。Tenant Admin 账号的登录名为平台运营自定义字符串,不受此约束。
**验收标准**
- [ ] 登录界面展示:租户品牌标识(公司 Logo + 公司名称)、手机号输入框、密码输入框、滑块拼图验证区域、「登录」按钮
- [ ] 手机号输入框 Placeholder「请输入您的手机号」仅接受数字字符非数字自动过滤固定 11 位
- [ ] 密码输入框默认密文显示,右侧提供「显示/隐藏」图标切换明密文
- [ ] **行为验证码(滑块拼图)**:展示一张带缺口的背景图和一块可拖动的拼图碎片,用户通过拖动滑块将碎片移动至缺口位置完成验证;无需输入任何字符,操作直观快速
- [ ] 验证逻辑:前端记录滑动轨迹(坐标序列 + 耗时),与背景图缺口位置一同发送至服务端;服务端综合校验**位置偏差**(允许 ±5px 容差)和**轨迹特征**(是否存在人类滑动的加速/减速规律)以区分机器行为
- [ ] 验证失败(位置不准或轨迹异常):拼图区域抖动动画提示失败,自动刷新新的背景图,用户重新拖动;**不计入账号密码错误次数**
- [ ] 验证成功后,拼图区域显示绿色对勾 + 「验证通过」文案,状态持续至本次登录提交完成
- [ ] 提供「刷新」图标按钮,允许用户主动刷新背景图(针对图片模糊或缺口不清晰的情况)
- [ ] 背景图从预置图库中随机抽取,缺口位置每次随机生成,防止固定模式被预测
- [ ] 三项(手机号、密码、验证码)均有填写后,「登录」按钮才可点击(否则置灰)
- [ ] 点击「登录」触发前端格式校验:
- 手机号为空 → 输入框下方红色提示「请输入手机号」
- 手机号不满 11 位 → 提示「请输入完整的 11 位手机号」
- 密码为空 → 提示「请输入密码」
- 验证码为空 → 提示「请完成滑块验证」
- [ ] 格式校验通过后,向服务端发起登录请求,按钮进入 loading 状态防止重复提交
- [ ] **登录成功(常规)**:服务端返回 Session Token 及 `is_initial_password` 标记;客户端存储 Token
-`is_initial_password = False`:直接跳转系统首页,顶部显示欢迎信息「欢迎回来,{姓名}」
-`is_initial_password = True`**立即跳转「修改初始密码」强制页面**,不可关闭、不可跳过、不可访问任何其他功能页面(详见 §5.3.4
- [ ] **登录失败(手机号或密码错误)**:显示「手机号或密码错误,请重新输入」(不区分具体原因,防止枚举攻击);验证码自动刷新;密码输入框清空;手机号保留
- [ ] **登录失败(验证码错误)**:显示「验证码有误,请重新输入」;验证码自动刷新;验证码输入框清空
- [ ] **账号被锁定**(同一账号密码连续错误 ≥ 5 次):显示「账号已被临时锁定,请 30 分钟后重试,或联系管理员解锁」;锁定状态下「登录」按钮置灰
- [ ] **账号已停用**:显示「账号已停用,请联系您的管理员」
- [ ] **Session 过期**:用户在系统内操作时 Session 过期,自动跳转至登录界面,并提示「登录已过期,请重新登录」
- [ ] 登录界面底部提供:「忘记密码」链接(详见 Story 3移除「忘记用户名」入口普通员工用户名即手机号无需找回Tenant Admin 如忘记用户名请联系平台运营)
---
### Story 3经纪人找回密码
**As** 忘记密码的经纪人,**I want** 通过手机号 + 短信验证码完成身份核验,重新设定密码,**So that** 无需邮箱、无需联系管理员,独立完成密码重置。
> **说明**考虑到大多数Agent经纪人没有常用邮箱本期找回密码统一通过短信验证码实现废弃邮箱找回方式。账号中 `email` 字段在本系统无任何必须业务用途,完全可选。
**验收标准**
- [ ] 点击登录界面「忘记密码」链接跳转至「找回密码」流程Stepper 分步页面,共三步)
**步骤一:输入手机号**
- [ ] 页面显示手机号输入框11 位数字,自动过滤非数字)、「获取验证码」按钮、「返回登录」链接
- [ ] 手机号为空或不足 11 位 → 点击「获取验证码」时在输入框下方提示「请输入完整的 11 位手机号」
- [ ] 手机号格式合法后,点击「获取验证码」,按钮进入 60 秒倒计时冷却态(「重新获取(59s)」),倒计时结束后按钮恢复可点击
- [ ] 服务端收到请求后:
- 若该手机号**存在**且账号状态为 `active`:向该号码发送 6 位数字短信验证码,有效期 **10 分钟**
- 若手机号**不存在**或账号已停用:页面统一提示「如该手机号已注册,验证码将在 1 分钟内发送」(**不泄露账号是否存在**
- [ ] 同一手机号 1 小时内最多发送 **5 次**短信验证码,超限后提示「发送次数过多,请 1 小时后再试」
- [ ] 短信内容模板「【Fonrey 房睿】您的密码重置验证码为 {code}10 分钟内有效,请勿泄露。」
**步骤二:输入短信验证码**
- [ ] 页面显示6 位验证码输入框(支持分格输入)、「重新发送」倒计时链接、「下一步」按钮
- [ ] 「下一步」按钮6 位验证码全部输入后方可点击
- [ ] 服务端校验验证码:
- 正确且未过期 → 进入步骤三,颁发一次性 `sms_reset_token`(有效期 15 分钟,一次性,服务端存储)
- 错误 → 提示「验证码有误,请重新输入」,错误次数 ≥ 5 次则本次验证码作废,需重新获取
- 已过期 → 提示「验证码已过期,请重新获取」
**步骤三:重置密码**
- [ ] 步骤三依赖步骤二颁发的 `sms_reset_token`(通过 URL 参数或会话状态传递Token 无效或过期 → 显示「操作已超时,请重新发起找回密码」,跳回步骤一
- [ ] **本页面复用「设置新密码」公共组件**(与首次登录强制修改密码页面为同一组件,详见 §5.3.4),保持 UI 与交互逻辑完全一致;入口上下文不同时,仅页面标题和提示文案有所差异:
| 元素 | 首次登录强制修改§5.3.4 | 找回密码步骤三(本 Story |
|------|--------------------------|--------------------------|
| 页面标题 | 「欢迎使用 Fonrey请先设置您的登录密码」 | 「重置您的登录密码」 |
| 提示文案 | 「您当前使用的是初始密码,为保障账号安全,请立即设置新密码后开始使用」 | 「请输入您的新密码,设置完成后请使用新密码重新登录」 |
| 提交按钮文案 | 「确认并进入系统」 | 「确认重置密码」 |
| 提交后跳转 | `is_initial_password = False`Session 保持,直接进入首页 | 所有 Session 立即失效,跳转登录界面并提示「密码已重置,请使用新密码登录」 |
- [ ] 提交成功后:`is_initial_password` 置为 **`False`**(找回密码属于用户主动操作,已完成身份核验,无需再触发强制修改流程)
> **注意**:与首次登录流程不同,找回密码时用户已通过短信验证码完成了身份核验,本次密码设置即视为"用户本人主动设置",不应再触发 `is_initial_password = True` 的二次强制修改。
---
### Story 4经纪人找回用户名已废弃
> **状态**已废弃。普通员工用户名固定为手机号无需找回Tenant Admin 如忘记用户名,请联系平台运营线下处理。本 Story 保留占位以维持版本记录,实现时跳过此 Story。
---
### Story 5手机验证码登录MVP 实现)
**As** 已有账号的经纪人,**I want** 通过手机号 + 短信验证码直接登录,**So that** 在忘记密码或不想输入密码时,仍能快速进入系统。
> **说明**:短信基础设施(`sms_otp_records` 表、OTP 发送/校验逻辑)已在 Story 3 找回密码中建设完成,本 Story 直接复用,实现成本极低。登录界面提供「密码登录」和「验证码登录」两个并列入口,用户自由切换,两种方式均为 MVP 正式功能。
**验收标准**
- [ ] 登录界面提供两种登录方式的切换 Tab**「密码登录」**(默认选中)和 **「验证码登录」**
- [ ] 切换 Tab 时,输入区域平滑切换,已填内容清空,滑块验证状态重置
**「验证码登录」界面元素**
- [ ] 手机号输入框(规格同 Story 211 位数字,自动过滤非数字)
- [ ] 验证码输入框6 位数字分格输入)+ 「获取验证码」按钮60 秒倒计时冷却态)
- [ ] 滑块拼图验证区域(规格同 Story 2**先通过滑块验证,再允许点击「获取验证码」**
- [ ] 「登录」按钮(手机号 + 验证码均填写后方可点击)
**获取验证码逻辑**
- [ ] 用户须先完成滑块验证,「获取验证码」按钮方可点击;未完成滑块时点击 → 提示「请先完成滑块验证」
- [ ] 点击「获取验证码」后,服务端:
- 手机号格式不合法 → 前端拦截,提示「请输入完整的 11 位手机号」
- 手机号存在且状态 `active` → 发送 6 位 OTP有效期 **5 分钟**,存入 `sms_otp_records``scene = 'login'`
- 手机号不存在或已停用 → 统一响应「如该手机号已注册,验证码将在 1 分钟内发送」(防止枚举攻击)
- [ ] 同一手机号 1 小时内最多发送 **10 次**登录验证码(找回密码为独立计数,两者不共享限额);超限后提示「发送次数过多,请 1 小时后再试」
- [ ] 短信内容模板「【Fonrey 房睿】您的登录验证码为 {code}5 分钟内有效,请勿泄露。」
**登录校验逻辑**
- [ ] 点击「登录」,服务端校验 OTP
- 正确且未过期 → 登录成功,后续行为与 Story 2 密码登录完全一致(含 `is_initial_password` 判断)
- 错误 → 提示「验证码有误,请重新输入」;连续错误 ≥ 5 次 → 本次 OTP 作废,提示「验证码已失效,请重新获取」
- 已过期 → 提示「验证码已过期,请重新获取」
- [ ] **账号被锁定**(密码登录失败次数触发):验证码登录仍受账号锁定限制,锁定期间无法通过任何方式登录,提示「账号已被临时锁定,请 30 分钟后重试,或联系管理员解锁」
> **设计说明**:账号锁定是账号维度的安全策略,不区分登录方式;否则锁定形同虚设。
- [ ] **账号已停用**:提示「账号已停用,请联系您的管理员」
**接口规范**
```
POST /api/auth/login/phone/
Request: { phone: string, sms_code: string }
Response: { token: string, is_initial_password: bool, user: {...} } | { error_code: string, message: string }
```
---
### Story 6预留——微信扫码登录接口预留v2 实现)
**As** 绑定了微信账号的经纪人,**I want** 在登录界面扫描微信二维码完成登录,**So that** 免去输入账号密码的步骤,提升登录体验。
**当前状态**:本期 UI 入口以「即将开放」禁用态展示于登录界面,接口定义预留,不开放实际功能。
**预留接口设计**(供后端提前规划):
```
GET /api/auth/wechat/qrcode/ # 获取微信扫码二维码(含 state + 有效期)
POST /api/auth/wechat/callback/ # 微信扫码确认后回调,换取系统 Token
```
**绑定条件**v2 实现时的前置要求):
- 微信账号必须先在「个人设置」中与用户名账号完成绑定
- 二维码有效期 3 分钟,过期后前端自动刷新二维码
- 微信账号只能绑定一个用户名账号(同一租户内)
---
## 5. 功能详细说明
### 5.1 客户端 Tenant 识别流程
#### 5.1.1 流程概述
```
客户端启动
├─ 本地有 Tenant Code 缓存?
│ │
│ YES ──→ 校验缓存 Tenant Code 是否仍有效(服务端 validate
│ │
│ 有效 ──→ 直接进入登录界面
│ │
│ 无效 ──→ 清除缓存,进入 Tenant 识别界面
└─ NO ──→ 显示 Tenant 识别界面
用户输入 Tenant Code → 发起验证
验证成功 ──→ 缓存 Tenant Code → 进入登录界面
验证失败 ──→ 显示错误信息,保持识别界面
```
#### 5.1.2 Tenant 识别界面规范
| 元素 | 规格 |
| ------------- | --------------------------------------- |
| 页面背景 | 品牌色渐变(与登录界面保持一致的视觉风格) |
| Logo | Fonrey 产品 Logo居中显示 |
| 标题 | 「欢迎使用 Fonrey 房睿」 |
| 副标题 | 「请输入您公司的专属识别码以继续」 |
| Tenant Code 输入框 | 单行数字输入,固定 12 位,支持粘贴;非数字字符自动过滤,超出 12 位截断 |
| 输入框 Label | 「公司识别码Tenant Code」 |
| 确认按钮 | 主色调按钮,文字「确认」 |
| 错误提示 | 输入框下方红色文字,固定区域占位(不影响布局抖动) |
| 帮助文案 | 「不知道识别码请联系您公司的Tenant Admin租户管理员」 |
#### 5.1.3 Tenant Code 格式规范
- **格式**:固定 **12 位纯数字**,如 `202500010001`
- **生成规则**(建议):由平台运营在系统管理后台开通租户时自动生成,不允许手动指定,确保全局唯一性;可采用时间戳前缀 + 随机后缀的方式生成(如 `YYYYMM` + 6 位随机数)
- **前端校验**:输入框仅接受数字字符(非数字自动过滤),输入满 12 位后自动触发格式完成状态;少于 12 位时点击「确认」弹出提示「识别码须为 12 位数字」
- **唯一性**:全局唯一(公共 Schema 层面),同一 Tenant Code 不可分配给多个租户
- **客户端存储**Electron `app.getPath('userData')` 目录下的配置文件(加密存储,防止明文读取)
#### 5.1.4 服务端 Tenant 验证接口规范
```
POST /api/auth/tenant/verify/
Request Body:
{
"tenant_code": "202500010001"
}
Response 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/"
}
Response 200 (失败):
{
"valid": false,
"error_code": "TENANT_NOT_FOUND",
"message": "识别码无效"
}
```
> **注意**:该接口属于 `shared_apps` 范围,路由在公共 Schema 下,不需要租户鉴权,但需要限流保护(每 IP 每分钟 ≤ 10 次请求)。
---
### 5.2 登录界面设计规范
#### 5.2.1 界面布局
登录界面顶部以 **Tab 切换**区分两种登录方式「密码登录」默认选中Tab 下方的表单区随当前选中 Tab 动态切换,微信扫码作为独立的「其他登录」保持禁用。
```
┌─────────────────────────────────────────┐
│ [租户 Logo] [租户公司名称] │ ← 顶部品牌区Tenant 识别后回填)
│ │
│ ┌──────────────┬──────────────────┐ │
│ │ 密码登录 ✓ │ 验证码登录 │ │ ← 登录方式 Tab默认选中「密码登录」
│ └──────────────┴──────────────────┘ │
│ │
│ ── 密码登录 Tab默认展示─────────── │
│ ┌───────────────────────┐ │
│ │ 手机号 │ │
│ └───────────────────────┘ │
│ ┌───────────────────────┐ │
│ │ 密码 👁 │ │
│ └───────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ [背景图 + 拼图缺口] 🔄 │ │ ← 右上角刷新图标
│ │ [拼图碎片] ◀ 拖动滑块完成拼图 ▶│ │
│ └─────────────────────────────────┘ │
│ ┌───────────────────────┐ │
│ │ 登 录 │ │ ← 主 CTA橙色
│ └───────────────────────┘ │
│ 忘记密码 │ ← 文字链接(忘记用户名入口已废弃)
│ │
│ ── 验证码登录 Tab切换后展示──────── │
│ ┌───────────────────────┐ │
│ │ 手机号 │ │
│ └───────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ [拼图验证] ← 先完成验证,再获取 │ │ ← 验证码登录下滑块前置
│ └─────────────────────────────────┘ │
│ ┌──────────────────┐ ┌─────────────┐ │
│ │ 验证码6 位) │ │ 获取验证码 │ │ ← 通过滑块后按钮可点击60s 冷却
│ └──────────────────┘ └─────────────┘ │
│ ┌───────────────────────┐ │
│ │ 登 录 │ │
│ └───────────────────────┘ │
│ 忘记密码 │
│ │
│ ─────────────── 其他登录 ──────────────│
│ [微信扫码登录 - 即将开放] │ ← 禁用态,灰色
│ │
│ 切换公司 │ ← 底部,小字链接
└─────────────────────────────────────────┘
```
#### 5.2.2 安全机制
| 机制 | 规格 |
|------|------|
| 验证码类型 | **滑块拼图行为验证码**:展示带缺口的背景图 + 可拖动的拼图碎片,用户滑动碎片至缺口完成验证,无需输入字符 |
| 验证逻辑 | 服务端综合校验**位置偏差**(缺口中心 ±5px 容差)+ **滑动轨迹特征**(加速/减速曲线、总耗时),双重判断是否为人类行为 |
| 背景图来源 | 预置图库随机抽取,缺口位置每次服务端随机生成,防止固定模式被预测 |
| 验证码有效期 | 单次验证会话有效,提交登录后服务端 Token 立即失效;超过 3 分钟未操作需重新加载 |
| 验证失败处理 | 拼图区域抖动动画提示,自动刷新新背景图;**不计入账号密码错误次数**(行为验证失败属独立事件) |
| 密码错误锁定 | 同一账号连续密码错误 ≥ 5 次,锁定 30 分钟;解锁方式:等待超时自动解锁 或 管理员手动解锁 |
| 密码错误计数 | 计数存于 RedisKey 格式:`login_fail:tenant_id:phone`phone 即用户名/手机号TTL 30 分钟 |
| 验证码刷新 | 登录失败(用户名/密码错误)后自动刷新拼图;用户亦可主动点击「刷新」图标重新加载背景图 |
| HTTPS | 所有登录相关请求强制 HTTPS不允许 HTTP 降级 |
| 密码传输 | 前端不做密码加密HTTPS 层保证传输安全;后端存储使用 `django.contrib.auth` 默认的 `PBKDF2+SHA256` 哈希 |
| Session 有效期 | 默认 8 小时(工作日单日使用场景);可由租户管理员在「系统设置」中调整 |
---
### 5.3 账号与员工实名绑定规范
#### 5.3.1 绑定原则
- 每个系统登录账号必须与「组织人事管理」模块中的一条**员工档案Staff**绑定
- 账号与员工是 **1:1 关系**,一个员工对应一个账号,一个账号只能绑定一个员工
- **不支持用户自行注册**,所有账号均由有权限的管理角色创建
#### 5.3.2 账号创建权限分层
系统内共有两类账号创建场景,权限和规则各不相同:
**② Tenant Admin 账号(每个租户的超级管理账号)**
| 项目 | 规格 |
| ---- | ------------------------------------------------------------ |
| 创建时机 | 平台运营在系统管理后台开通租户时,系统**自动**以该租户联系人手机号创建 Tenant Admin 账号,无需手动设置 |
| 用户名 | **固定为该租户联系人的手机号**11 位数字),全局唯一,创建后不可更改 |
| 初始密码 | **系统统一固定初始密码**(与普通员工相同,由平台在部署配置中设定,如 `Fonrey@2025` |
| 首次登录 | 强制修改初始密码,不可跳过 |
| 权限范围 | 拥有该租户内最高权限,可管理员工账号、角色、系统设置等 |
| 数量限制 | 每个租户仅限 1 个 Tenant Admin 账号后续可扩展为多管理员v2 规划) |
| 数据来源 | 联系人手机号来自 `public.tenants.contact_phone` 字段,开通租户时由平台运营录入,必填 |
**① 普通员工账号(经纪人、店长、行政等)**
| 项目 | 规格 |
|------|------|
| 创建时机 | Tenant Admin 在「组织人事管理 → 新增员工」时,系统自动为该员工创建登录账号 |
| 用户名 | **固定为该员工的手机号**11 位数字),同租户内唯一,创建后不可更改 |
| 初始密码 | **系统统一固定初始密码**(由平台在部署配置中设定,如 `Fonrey@2025`),所有新员工账号均使用同一初始密码 |
| 首次登录 | 强制修改初始密码,**不可跳过**(详见 5.3.4 |
| 密码重置 | Tenant Admin 可在员工管理界面对任意员工账号执行「重置密码」,重置后恢复为固定初始密码,触发首次登录强制修改流程 |
| 账号禁用 | 员工离职或被停用时,对应账号自动禁用;禁用账号无法登录,历史操作记录保留 |
#### 5.3.3 账号字段规范
| 字段 | 类型 | Tenant Admin | 普通员工账号 | 说明 |
|------|------|-------------|-------------|------|
| 用户名username | CharField(30) | **固定为联系人手机号**11 位数字) | **固定为员工手机号**11 位数字) | 登录 ID创建后不可更改两类账号规则统一 |
| 密码password | CharField | **系统统一固定初始密码** | **系统统一固定初始密码** | PBKDF2+SHA256 哈希存储;首次登录强制修改 |
| 手机号phone | CharField(11) | **必填,同时作为用户名**,来源于 `public.tenants.contact_phone` | **必填,同时作为用户名**,加密存储,同租户内唯一 | 两类账号均用手机号登录v2 启用手机验证码后复用此字段 |
| 邮箱email | EmailField | 选填,同租户唯一 | 选填,同租户唯一 | 在本系统无必须业务用途,完全可选;普通员工忘记密码通过手机短信验证码自助找回,**与邮箱无关** |
| 员工档案关联staff_id | OneToOneField → `org.Staff` | 可选关联(平台运营账号) | 必须关联 | 实名绑定 |
| 账号状态status | CharField | `active` / `disabled` / `locked` | `active` / `disabled` / `locked` | locked 为密码错误锁定30 分钟自动恢复 |
| 初始密码标记is_initial_password | BooleanField | True首次登录前 | True首次登录前 | True 时登录成功后强制跳转修改密码页 |
| 创建人created_by | ForeignKey → self | 平台运营(系统管理后台) | Tenant Admin | 审计追溯 |
#### 5.3.4 首次登录强制修改密码
- 新员工账号创建后,`is_initial_password = True`,账号处于「初始密码」状态
- 员工使用手机号(用户名)+ 固定初始密码登录成功后,系统**立即跳转**至「修改初始密码」强制页面,**不可关闭、不可跳过**,任何其他系统功能页面均不可访问
- Tenant Admin 对员工账号执行「重置密码」后,`is_initial_password` 重置为 True该员工下次登录时再次触发强制修改流程
- 修改成功后,`is_initial_password` 更新为 False原 Session 保持有效,直接进入系统首页
**强制修改密码页面规范**
| 元素 | 规格 |
|------|------|
| 页面标题 | 「欢迎使用 Fonrey请先设置您的登录密码」 |
| 提示文案 | 「您当前使用的是初始密码,为保障账号安全,请立即设置新密码后开始使用」 |
| 新密码输入框 | 密文显示,右侧「显示/隐藏」切换 |
| 确认新密码输入框 | 密文显示,与新密码一致性实时校验 |
| 密码强度提示 | 逐条实时显示规则达标状态(✓/✗):长度 ≥ 8 位 / 包含字母 / 包含数字 |
| 提交按钮 | 「确认并进入系统」 |
| 不可操作项 | 无「跳过」按钮;顶部导航栏、侧边菜单、关闭按钮均禁用 |
---
### 5.4 找回密码详细说明
> **说明**Story 4「找回用户名」已废弃。普通员工用户名固定为手机号无需找回Tenant Admin 如忘记用户名请联系平台运营线下处理。
#### 5.4.1 找回密码流程
```
用户点击「忘记密码」
步骤1输入手机号
├─ 输入 11 位手机号,点击「获取验证码」
│ │
│ 服务端校验手机号是否存在且状态为 active
│ │
│ 统一响应「如该手机号已注册,验证码将在 1 分钟内发送」(防止枚举)
│ │
│ 后台:存在且 active → 生成 6 位 OTP有效期 10 分钟,存入 sms_otp_records → 发送短信
│ 不存在或已停用 → 静默处理
步骤2输入短信验证码
├─ 输入 6 位验证码,点击「下一步」
│ │
│ 服务端校验 OTP
│ │
│ 正确且未过期 → 颁发一次性 sms_reset_token有效期 15 分钟)→ 进入步骤3
│ │
│ 错误(累计 < 5 次)→ 提示「验证码有误,请重新输入」
│ 错误(累计 ≥ 5 次)→ 提示「验证已失败,请重新获取验证码」,本次 OTP 作废
│ 已过期 → 提示「验证码已过期,请重新获取」
步骤3重置密码
├─ 页面携带 sms_reset_token服务端校验有效性
│ │
│ 无效/过期 → 提示「操作已超时请重新发起找回密码」跳回步骤1
│ │
├─ 用户输入新密码 + 确认新密码,实时逐条校验复杂度规则(✓/✗)
└─ 提交成功
→ 更新密码is_initial_password = False
→ 清除该账号所有有效 Session强制重新登录
→ 跳转登录界面,提示「密码已重置,请使用新密码登录」
```
---
### 5.5 手机验证码登录详细说明
> 本节为 Story 5 的实现规范补充。短信基础设施(`sms_otp_records` 表、OTP 发送/校验逻辑)在 Story 3 找回密码中已建设完成,本节描述在**登录场景**下复用该基础设施时的关键差异点。
**与找回密码短信逻辑的差异对比**
| 维度 | 找回密码Story 3 | 验证码登录Story 5 |
|------|-------------------|---------------------|
| `scene` 字段 | `password_reset` | `login` |
| OTP 有效期 | 10 分钟 | 5 分钟 |
| 每小时发送上限 | 5 次 | 10 次 |
| 验证成功后动作 | 颁发 `sms_reset_token` → 步骤三重置密码 | 直接颁发 Session Token登录成功 |
| 短信文案 | 「密码重置验证码」 | 「登录验证码」 |
| 账号锁定影响 | 不受密码错误锁定限制(非密码登录路径) | **受账号锁定限制**(账号维度安全策略,不区分方式)|
**滑块验证前置规则**(验证码登录特有):
- 用户须先完成滑块拼图验证,「获取验证码」按钮方可点击
- 滑块验证通过后,拼图区域保持「验证通过」状态,不需要在点击「登录」前再次验证
- 切换 Tab 时,滑块验证状态重置(须重新完成验证后方可获取验证码)
**`sms_otp_records` 表复用说明**
- 不新建表,复用 `DATA_MODEL_LOGIN.md` 中定义的 `sms_otp_records`
- `scene` 字段区分场景:`login` / `password_reset`,各自独立限流计数
- 同一手机号同一 scene 同一时间只有一条有效 OTP新发送时将旧记录标记为 `used`
---
### 5.6 后端数据模型设计
> **数据模型已迁移至独立文档**,请参阅:
> **`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md`**
该文档包含:
- `user_accounts` 账号主表完整字段定义、约束、索引、Django Model 代码)
- `login_attempts` 登录审计表
- `sms_otp_records` 短信验证码记录表(找回密码 + 验证码登录共用)
- `password_histories` 历史密码记录表
- Redis 缓存结构说明
- 账号状态机与创建流程
-`org.Staff` 的关联规则及跨 App 依赖设计
- Django Migrations 迁移顺序说明
- 架构决策说明ADR
---
### 5.7 Electron 客户端登录相关约定
| 约定项 | 规格 |
|--------|------|
| Tenant Code 存储 | `electron-store``app.getPath('userData')` + AES 加密,不存储明文 |
| Session Token 存储 | 内存(`global` 变量)+ `session` CookieChromium 管理),不写入磁盘明文文件 |
| 登录页加载 | 客户端主进程根据 Tenant Code 构建目标 URL`https://{tenant_slug}.fonrey.com/auth/login/`),通过 `BrowserWindow.loadURL()` 加载 |
| 多标签页处理 | 同一 `BrowserWindow` 内,所有页面共享同一 Session Cookie |
| 客户端登出 | 调用服务端 `POST /api/auth/logout/` 使服务端 Session 失效 + 清除 Chromium Session Cookie |
| 窗口关闭时 | Session 保留(不自动登出),下次打开客户端时若 Session 未过期,直接进入系统 |
| 强制更新场景 | 若客户端版本低于服务端 `min_required_version`,则在登录界面前先展示「请更新客户端」提示,阻断登录流程(参见发布管理模块 PRD|
---
## 6. 技术注意事项
### 6.1 依赖与技术选型
| 依赖项 | 用途 | 说明 |
|--------|------|------|
| `django.contrib.auth` | 用户认证基础框架 | 扩展 `AbstractBaseUser` 而非直接使用 `User` 模型,以支持 `username` 唯一性约束在租户维度而非全局 |
| `django-tenants` | 多租户隔离 | `UserAccount` 属于租户级 SchemaTenant 验证接口属于 `shared_apps` |
| `Redis` | 滑块验证 Token 存储、登录失败计数、短信 OTP 限流计数 | 验证 Key`captcha_token:{uuid}`TTL 3min登录失败 Key`login_fail:{tenant_id}:{username}`OTP 限流 Key`sms_limit:{scene}:{phone}`TTL 1h|
| 短信服务(待选型) | 发送登录验证码 / 找回密码验证码 | 国内需选用具备短信资质的服务商(如阿里云短信、腾讯云短信);需申请短信签名和模板审核 |
| `Celery` | 异步任务处理 | 短信发送异步处理,防止接口响应超时;原邮件发送需求已废弃,短信为主要通知方式 |
| `django-ratelimit` 或自定义中间件 | 接口限流 | Tenant 验证接口、登录接口、找回密码接口均需限流 |
| `Pillow` | 滑块拼图图片处理 | 生成拼图背景图(抠出缺口区域)及对应的拼图碎片图片,输出为 Base64分别通过两个字段返回给前端 |
### 6.2 多租户下的 `UserAccount` 隔离
- `UserAccount` 表位于**租户 Schema 内**`django-tenants` 租户隔离范围),因此 username 唯一性约束在租户维度生效,不同租户的经纪人可以有相同用户名
- Tenant 验证接口(`/api/auth/tenant/verify/`)位于**公共 Schema**`shared_apps`),使用 `TenantModel` 查询
- 登录、找回密码等接口通过请求域名(`{tenant_slug}.fonrey.com`)切换到对应租户 Schema`django-tenants` 中间件自动处理)
### 6.3 已知风险
| 风险 | 可能性 | 影响 | 缓解措施 |
|------|--------|------|---------|
| 滑块验证被机器模拟轨迹绕过 | 低 | 高 | 服务端同时校验位置偏差 + 轨迹曲线特征(非线性运动特征),拒绝匀速/程序化轨迹;后续可引入设备指纹加固 |
| Tenant Code 枚举攻击(暴力试探) | 低 | 中 | Tenant 验证接口限流每IP每分钟≤10次返回结果不区分「未找到」与「已禁用」|
| 密码重置 Token 泄露 | 低 | 高 | `sms_reset_token` 单次有效、15 分钟过期、HTTPS 传输 |
| 短信服务故障导致用户无法找回密码或验证码登录 | 中 | 高 | 短信发送失败写入告警日志;密码登录作为保底方式(非单一入口);建议配置备用短信服务商通道 |
| 多端同时登录同一账号 | 高(日常场景) | 低 | 本期允许,后续如需踢出,可在 Token 机制中引入版本号 |
### 6.4 开放问题(开发前需确认)
- [ ] **短信服务商选型**:使用阿里云短信 / 腾讯云短信 / 其他服务商?需运维确认并提前申请短信签名和模板审核(国内审核周期 13 个工作日)— 负责人:后端负责人 — 截止:开发启动前
- [ ] **Session 有效期默认值**8 小时是否满足各租户需求?是否允许租户管理员自行配置?— 负责人:产品经理 — 截止:开发启动前
- [ ] **滑块拼图实现方案**自研Pillow 生成图片 + 前端拖拽组件)还是集成第三方行为验证服务(如极验 GeeTest / 网易易盾)?自研可控但需维护图库;第三方开箱即用但引入外部依赖,需评估数据合规要求 — 负责人:后端负责人 + 安全 — 截止:开发启动前
- [ ] **账号锁定通知**:账号被锁定后,是否自动发短信通知用户和/或通知管理员(站内消息)?— 负责人:产品经理 — 截止:开发启动前
- [ ] **历史密码校验范围**:最近 3 次是否足够?是否需要额外规则(如不能与用户名相同)?— 负责人:产品经理 — 截止:开发启动前
---
## 7. 发布计划
| 阶段 | 时间 | 受众 | 准入门槛 |
|------|------|------|---------|
| 内部 Alpha | 待定 | 研发团队 + 1 家种子租户 | 核心流程Tenant 识别 + 账密登录 + 找回密码)无 P0 Bug |
| 封闭 Beta | 待定 | 5 ~ 10 家测试租户 | 登录成功率 ≥ 99%,验证码拦截机制正常运作 |
| 正式发布 | 待定 | 全量租户 | Beta 阶段无未修复的安全漏洞;帮助文档发布 |
**回滚标准**:若正式发布后 24 小时内登录失败率(非验证码拦截原因)超过 2%,或出现账号数据泄露事件,立即回滚并启动安全审查。
---
## 8. 附录
### 8.1 登录状态流转图
```
[未识别 Tenant]
│ 输入有效 Tenant Code
[未登录]
│ 账密登录成功
[初始密码状态](如账号为初始密码)
│ 强制修改密码成功
[已登录 - Active Session]
│ Session 过期 / 主动登出 / 管理员强制登出
[未登录](跳转登录界面)
[账号锁定状态]5次错误后
│ 30 分钟后自动解锁 或 管理员手动解锁
[未登录](可重新登录)
```
### 8.2 接口清单汇总
| 接口 | 方法 | Schema 位置 | 是否需要鉴权 | 说明 |
|------|------|------------|------------|------|
| `/api/auth/tenant/verify/` | POST | Publicshared | 否 | Tenant Code 验证 |
| `/api/auth/captcha/` | GET | Tenant | 否 | 获取滑块拼图验证码(返回背景图 Base64 + 碎片图 Base64 + 验证 Token |
| `/api/auth/captcha/verify/` | POST | Tenant | 否 | 提交滑动轨迹 + 位置,服务端校验并返回一次性通过凭证(供登录接口使用) |
| `/api/auth/login/` | POST | Tenant | 否 | 手机号 + 密码登录 |
| `/api/auth/login/phone/` | POST | Tenant | 否 | 手机号 + 短信验证码登录MVP 正式功能) |
| `/api/auth/logout/` | POST | Tenant | 是 | 登出,使 Session 失效 |
| `/api/auth/recover/password/request/` | POST | Tenant | 否 | 发起找回密码(发送短信验证码) |
| `/api/auth/recover/password/verify/` | POST | Tenant | 否 | 校验短信验证码,颁发一次性 `sms_reset_token` |
| `/api/auth/recover/password/reset/` | POST | Tenant | 否Token 鉴权) | 提交新密码 |
| `/api/auth/wechat/qrcode/` | GET | Tenant | 否 | **预留 v2**,获取微信二维码 |
| `/api/auth/wechat/callback/` | POST | Tenant | 否 | **预留 v2**,微信扫码回调 |
### 8.3 相关文档参考
- 技术栈文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
- **登录管理数据模型**`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md`
- **登录管理技术方案**`Project/fonrey/TECH_STACK/登录管理技术方案.md`