Sync: add agent design notes
This commit is contained in:
632
Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md
Normal file
632
Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md
Normal file
@@ -0,0 +1,632 @@
|
||||
# PRD:用户登录管理模块
|
||||
|
||||
**状态**: Draft
|
||||
**作者**: 产品经理
|
||||
**最后更新**: 2026-04-24(v1.1 验证码方式由图形字符验证码更新为滑块拼图行为验证码)
|
||||
**版本**: 1.1
|
||||
**所属系统**: Fonrey 房产经纪管理系统
|
||||
**关联模块**: 组织人事管理、权限管理、系统管理
|
||||
|
||||
---
|
||||
|
||||
## 1. 问题陈述
|
||||
|
||||
### 1.1 背景
|
||||
|
||||
Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用多租户架构(`django-tenants` + PostgreSQL Schema 隔离)。终端用户通过 **Windows 桌面客户端(Electron)** 使用系统,无需手动输入网址即可打开 Web 应用。
|
||||
|
||||
在多租户环境下,用户的身份验证流程比单租户系统更复杂:
|
||||
|
||||
- 用户安装客户端后,系统必须先识别当前设备归属哪个租户,才能加载对应租户的登录界面和数据隔离环境
|
||||
- 每家经纪公司作为独立租户,其员工账号、组织结构、数据均完全隔离
|
||||
- 经纪人账号须与实名员工档案绑定,确保每一条操作记录可追溯至具体自然人
|
||||
- 现阶段登录方式以账号密码为主;手机验证码登录、微信扫码登录需预留接口,待移动端(小程序)上线后实现
|
||||
|
||||
### 1.2 核心痛点
|
||||
|
||||
| 痛点 | 影响方 | 当前代价 |
|
||||
|------|--------|---------|
|
||||
| 多租户环境下,客户端不知道应该连接哪个租户的服务端 | 新用户首次安装后无法正常使用 | 系统无法启动,用户体验极差 |
|
||||
| 账号密码裸露登录,缺乏验证码保护 | 所有用户 | 存在暴力破解、自动化恶意登录风险 |
|
||||
| 用户忘记账号或密码无自助找回通道 | 一线经纪人 | 依赖管理员手动重置,效率低 |
|
||||
| 账号未与实名经纪人档案绑定 | 系统管理员、合规审计 | 操作行为无法追溯至自然人 |
|
||||
| 无多因素认证,安全系数低 | 管理层、数据合规 | 存在账号冒用、数据泄露风险 |
|
||||
|
||||
### 1.3 目标用户
|
||||
|
||||
| 角色 | 描述 | 使用频率 |
|
||||
|------|------|----------|
|
||||
| 一线经纪人 | 每日登录系统使用房源/客源功能 | 每日高频 |
|
||||
| 店长 / 经理 | 登录后查看全店数据、管理任务 | 每日 |
|
||||
| 系统管理员 | 管理账号创建、密码重置、租户初始化 | 按需 |
|
||||
| 新安装用户 | 首次安装客户端后需完成 Tenant 识别 | 一次性 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 目标与成功指标
|
||||
|
||||
| 目标 | 指标 | 当前基准 | 目标值 | 衡量周期 |
|
||||
|------|------|----------|--------|----------|
|
||||
| 降低登录失败率 | 账号密码正确情况下的登录成功率 | 待统计 | ≥ 99% | 上线后 30 天 |
|
||||
| 防止恶意登录 | 每日验证码拦截异常登录请求数 | 0(无保护) | 建立基线,同IP异常次数 > 5次/分钟触发封锁 | 上线后持续监控 |
|
||||
| 提升找回账号效率 | 用户自助找回密码耗时 | 依赖管理员(约1工作日) | < 3 分钟(邮件/短信自助) | 上线后 30 天 |
|
||||
| 确保账号实名绑定率 | 拥有系统账号且未与员工档案绑定的账号比例 | 待统计 | 0%(强制绑定) | 上线即达标 |
|
||||
| Tenant 识别成功率 | 首次安装后成功完成 Tenant 识别的用户比例 | 待统计 | ≥ 98% | 上线后 30 天 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 非目标(本期不做)
|
||||
|
||||
- **手机验证码登录**:移动端小程序上线后实现,本期**接口预留**,UI 入口以「即将开放」禁用态展示
|
||||
- **微信扫码登录**:移动端小程序上线后实现,本期**接口预留**,UI 入口以「即将开放」禁用态展示
|
||||
- **单点登录(SSO)/ 企业微信集成**:后续版本规划
|
||||
- **多设备并发登录的强制踢出策略**:本期允许同账号多端登录,后续安全策略模块规划
|
||||
- **登录时段限制 / IP 白名单**:安全策略模块另行规划
|
||||
- **管理后台(Platform Admin)登录**:系统管理员登录管理后台的流程属于系统管理模块,本 PRD 专注租户内用户登录
|
||||
|
||||
---
|
||||
|
||||
## 4. 用户故事与验收标准
|
||||
|
||||
---
|
||||
|
||||
### Story 1:新用户首次启动客户端——Tenant 识别
|
||||
|
||||
**As** 新安装 Fonrey 客户端的经纪人,**I want** 在首次启动时输入所属公司的 Tenant ID 完成租户识别,**So that** 客户端能连接到正确的服务端,后续显示对应公司的登录界面和数据。
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 客户端首次启动时(本地无 Tenant ID 缓存),自动呈现「Tenant 识别」界面,而非直接显示登录界面
|
||||
- [ ] 界面包含:产品 Logo、产品名称「Fonrey 房睿」、说明文案「请输入您公司的专属识别码」、Tenant ID 输入框、「确认」按钮
|
||||
- [ ] Tenant ID 输入框支持粘贴操作,自动去除前后空格
|
||||
- [ ] 点击「确认」后,客户端向服务端发起 Tenant 验证请求(`POST /api/auth/tenant/verify/`),展示加载状态(spinner)
|
||||
- [ ] **验证成功**:服务端返回租户名称及品牌信息(如公司名称、Logo URL);客户端将 Tenant ID 写入本地持久化存储,自动跳转至该租户的登录界面;界面顶部展示「正在登录:XX 房产」
|
||||
- [ ] **验证失败(Tenant ID 无效)**:输入框下方显示红色错误提示「识别码无效,请联系您的系统管理员获取正确的识别码」;Tenant ID 不写入本地缓存;用户可重新输入
|
||||
- [ ] **网络异常**:显示「网络连接失败,请检查网络后重试」,提供「重试」按钮
|
||||
- [ ] 非首次启动(本地已有合法 Tenant ID 缓存):直接跳过识别界面,进入登录界面
|
||||
- [ ] 登录界面提供「切换公司」入口(链接文字,非主要 CTA),点击后清除本地 Tenant ID 缓存并重新显示 Tenant 识别界面;确认前弹出二次确认「切换公司将退出当前账号,是否继续?」
|
||||
- [ ] Tenant 验证接口属于公开接口,无需鉴权;但需对单 IP 请求频率限制(每分钟 ≤ 10 次)以防止枚举攻击
|
||||
|
||||
---
|
||||
|
||||
### Story 2:经纪人通过账号密码登录
|
||||
|
||||
**As** 已识别租户的经纪人,**I want** 通过用户名和密码完成登录,**So that** 进入系统开始工作。
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 登录界面展示:租户品牌标识(公司 Logo + 公司名称)、用户名输入框、密码输入框、滑块拼图验证区域、「登录」按钮
|
||||
- [ ] 用户名输入框 Placeholder:「请输入用户名」;支持英文字母、数字、下划线,最大长度 50 字符
|
||||
- [ ] 密码输入框默认密文显示,右侧提供「显示/隐藏」图标切换明密文
|
||||
- [ ] **行为验证码(滑块拼图)**:展示一张带缺口的背景图和一块可拖动的拼图碎片,用户通过拖动滑块将碎片移动至缺口位置完成验证;无需输入任何字符,操作直观快速
|
||||
- [ ] 验证逻辑:前端记录滑动轨迹(坐标序列 + 耗时),与背景图缺口位置一同发送至服务端;服务端综合校验**位置偏差**(允许 ±5px 容差)和**轨迹特征**(是否存在人类滑动的加速/减速规律)以区分机器行为
|
||||
- [ ] 验证失败(位置不准或轨迹异常):拼图区域抖动动画提示失败,自动刷新新的背景图,用户重新拖动;**不计入账号密码错误次数**
|
||||
- [ ] 验证成功后,拼图区域显示绿色对勾 + 「验证通过」文案,状态持续至本次登录提交完成
|
||||
- [ ] 提供「刷新」图标按钮,允许用户主动刷新背景图(针对图片模糊或缺口不清晰的情况)
|
||||
- [ ] 背景图从预置图库中随机抽取,缺口位置每次随机生成,防止固定模式被预测
|
||||
- [ ] 三项(用户名、密码、验证码)均有填写后,「登录」按钮才可点击(否则置灰)
|
||||
- [ ] 点击「登录」触发前端格式校验:
|
||||
- 用户名为空 → 输入框下方红色提示「请输入用户名」
|
||||
- 密码为空 → 提示「请输入密码」
|
||||
- 验证码为空 → 提示「请输入验证码」
|
||||
- [ ] 格式校验通过后,向服务端发起登录请求,按钮进入 loading 状态防止重复提交
|
||||
- [ ] **登录成功**:服务端返回 Session Token;客户端存储 Token;跳转至系统首页;顶部显示欢迎信息「欢迎回来,{姓名}」
|
||||
- [ ] **登录失败(用户名或密码错误)**:显示「用户名或密码错误,请重新输入」(不区分是用户名错误还是密码错误,防止枚举攻击);验证码自动刷新;密码输入框清空;用户名保留
|
||||
- [ ] **登录失败(验证码错误)**:显示「验证码有误,请重新输入」;验证码自动刷新;验证码输入框清空
|
||||
- [ ] **账号被锁定**(同一账号密码连续错误 ≥ 5 次):显示「账号已被临时锁定,请 30 分钟后重试,或联系管理员解锁」;锁定状态下「登录」按钮置灰
|
||||
- [ ] **账号已停用**:显示「账号已停用,请联系您的管理员」
|
||||
- [ ] **Session 过期**:用户在系统内操作时 Session 过期,自动跳转至登录界面,并提示「登录已过期,请重新登录」
|
||||
- [ ] 登录界面底部提供:「忘记用户名」链接、「忘记密码」链接(详见 Story 3、Story 4)
|
||||
|
||||
---
|
||||
|
||||
### Story 3:经纪人找回用户名
|
||||
|
||||
**As** 忘记用户名的经纪人,**I want** 通过绑定的邮箱或手机号找回用户名,**So that** 不依赖管理员也能自助恢复登录。
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 点击登录界面「忘记用户名」链接,跳转至「找回用户名」页面(或弹窗)
|
||||
- [ ] 找回方式(本期以邮箱为主,手机号为预留字段):
|
||||
- 邮箱找回:输入注册邮箱,系统校验邮箱是否与已知账号匹配,匹配成功则发送包含用户名的邮件至该邮箱
|
||||
- 手机号找回(预留,UI 入口以「即将开放」禁用态展示)
|
||||
- [ ] 邮箱输入框:格式校验(包含「@」和域名),错误时提示「请输入有效的邮箱地址」
|
||||
- [ ] 点击「发送」后:
|
||||
- 邮箱存在且已绑定账号 → 显示「用户名已发送至您的邮箱,请查收」;发送按钮进入 60 秒倒计时不可重复点击
|
||||
- 邮箱不存在 → **不提示「邮箱未注册」**(防止用户信息枚举),统一显示「如该邮箱已绑定账号,您将收到一封包含用户名的邮件」
|
||||
- [ ] 邮件内容:纯文本邮件,包含用户名、发送时间,及「如非本人操作请联系管理员」说明
|
||||
- [ ] 发送频率限制:同一邮箱 1 小时内最多发送 3 次
|
||||
- [ ] 提供「返回登录」链接
|
||||
|
||||
---
|
||||
|
||||
### Story 4:经纪人找回密码
|
||||
|
||||
**As** 忘记密码的经纪人,**I want** 通过已知用户名 + 绑定邮箱(或手机号)自助重置密码,**So that** 不依赖管理员也能快速恢复登录。
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 点击登录界面「忘记密码」链接,跳转至「找回密码」流程(分步骤页面或 Stepper 组件)
|
||||
|
||||
**步骤一:身份验证**
|
||||
|
||||
- [ ] 用户输入:用户名 + 邮箱(本期);手机号找回为预留入口(禁用态)
|
||||
- [ ] 服务端校验用户名与邮箱是否匹配,不泄露具体原因(统一提示「如信息匹配,重置链接将发送至您的邮箱」)
|
||||
- [ ] 校验通过后,向绑定邮箱发送含一次性重置链接的邮件;链接有效期 **30 分钟**,使用后立即失效
|
||||
- [ ] 同一账号 1 小时内最多发送 3 次重置邮件
|
||||
|
||||
**步骤二:重置密码**
|
||||
|
||||
- [ ] 用户点击邮件中的链接,跳转至「重置密码」页面(链接含加密 Token,服务端校验 Token 有效性)
|
||||
- [ ] Token 无效或已过期 → 显示「链接已过期或已使用,请重新申请」,提供「重新申请」按钮
|
||||
- [ ] 页面包含:新密码输入框、确认新密码输入框
|
||||
- [ ] **密码复杂度规则**(符合安全基线):
|
||||
- 长度 8 ~ 32 位
|
||||
- 必须包含字母(区分大小写)和数字
|
||||
- 建议包含特殊符号(非强制,但页面提示推荐)
|
||||
- 不得与最近 3 次历史密码相同
|
||||
- [ ] 两次密码输入不一致 → 提示「两次密码输入不一致」
|
||||
- [ ] 不符合复杂度 → 实时提示具体不满足的规则(逐条校验,红色 × / 绿色 ✓ 视觉指引)
|
||||
- [ ] 提交成功 → 显示「密码已重置,请使用新密码登录」,自动跳转至登录界面;原所有 Session 立即失效(强制重新登录)
|
||||
|
||||
---
|
||||
|
||||
### Story 5:预留——手机验证码登录(接口预留,v2 实现)
|
||||
|
||||
**As** 绑定了手机号的经纪人,**I want** 通过手机号 + 短信验证码快速登录,**So that** 在忘记密码时仍能正常登录系统。
|
||||
|
||||
**当前状态**:本期 UI 入口以「即将开放」禁用态展示于登录界面,接口定义预留,不开放实际功能。
|
||||
|
||||
**预留接口设计**(供后端提前规划):
|
||||
|
||||
```
|
||||
POST /api/auth/login/phone/
|
||||
Request: { phone: string, sms_code: string, tenant_id: string }
|
||||
Response: { token: string, user: {...} } | { error: string }
|
||||
```
|
||||
|
||||
**绑定条件**(v2 实现时的前置要求):
|
||||
- 手机号必须先在「个人设置」中与用户名账号完成绑定并通过验证
|
||||
- 一个手机号只能绑定一个用户名账号(同一租户内)
|
||||
- 绑定手机号后,可通过手机号 + 短信验证码联合登录
|
||||
|
||||
---
|
||||
|
||||
### 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 ID 缓存?
|
||||
│ │
|
||||
│ YES ──→ 校验缓存 Tenant ID 是否仍有效(服务端 validate)
|
||||
│ │
|
||||
│ 有效 ──→ 直接进入登录界面
|
||||
│ │
|
||||
│ 无效 ──→ 清除缓存,进入 Tenant 识别界面
|
||||
│
|
||||
└─ NO ──→ 显示 Tenant 识别界面
|
||||
│
|
||||
用户输入 Tenant ID → 发起验证
|
||||
│
|
||||
验证成功 ──→ 缓存 Tenant ID → 进入登录界面
|
||||
│
|
||||
验证失败 ──→ 显示错误信息,保持识别界面
|
||||
```
|
||||
|
||||
#### 5.1.2 Tenant 识别界面规范
|
||||
|
||||
| 元素 | 规格 |
|
||||
|------|------|
|
||||
| 页面背景 | 品牌色渐变(与登录界面保持一致的视觉风格) |
|
||||
| Logo | Fonrey 产品 Logo,居中显示 |
|
||||
| 标题 | 「欢迎使用 Fonrey 房睿」 |
|
||||
| 副标题 | 「请输入您公司的专属识别码以继续」 |
|
||||
| Tenant ID 输入框 | 单行文本,最大长度 64 字符,支持粘贴 |
|
||||
| 输入框 Label | 「公司识别码(Tenant ID)」 |
|
||||
| 确认按钮 | 主色调按钮,文字「确认」 |
|
||||
| 错误提示 | 输入框下方红色文字,固定区域占位(不影响布局抖动) |
|
||||
| 帮助文案 | 「不知道识别码?请联系您公司的系统管理员」 |
|
||||
|
||||
#### 5.1.3 Tenant ID 格式规范
|
||||
|
||||
- Tenant ID 由系统管理模块生成(每个租户在开通时由平台运营分配)
|
||||
- 格式建议:`tenant_<slug>`,如 `tenant_fonrey_demo`,仅包含字母、数字、下划线,不区分大小写
|
||||
- 长度:6 ~ 64 字符
|
||||
- 客户端存储位置:Electron `app.getPath('userData')` 目录下的配置文件(加密存储,防止明文读取)
|
||||
|
||||
#### 5.1.4 服务端 Tenant 验证接口规范
|
||||
|
||||
```
|
||||
POST /api/auth/tenant/verify/
|
||||
|
||||
Request Body:
|
||||
{
|
||||
"tenant_id": "tenant_fonrey_demo"
|
||||
}
|
||||
|
||||
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 界面布局
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ [租户 Logo] [租户公司名称] │ ← 顶部品牌区(Tenant 识别后回填)
|
||||
│ │
|
||||
│ ┌───────────────────────┐ │
|
||||
│ │ 用户名 │ │
|
||||
│ └───────────────────────┘ │
|
||||
│ ┌───────────────────────┐ │
|
||||
│ │ 密码 👁 │ │
|
||||
│ └───────────────────────┘ │
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │ [背景图 + 拼图缺口] 🔄 │ │ ← 右上角刷新图标
|
||||
│ │ │ │
|
||||
│ │ [拼图碎片] │ │
|
||||
│ │ ├────────────────────────────── │ │
|
||||
│ │ ◀ 拖动滑块完成拼图 ▶ │ │
|
||||
│ └─────────────────────────────────┘ │
|
||||
│ ┌───────────────────────┐ │
|
||||
│ │ 登 录 │ │ ← 主 CTA,橙色
|
||||
│ └───────────────────────┘ │
|
||||
│ │
|
||||
│ 忘记用户名 忘记密码 │ ← 次级入口,文字链接
|
||||
│ │
|
||||
│ ─────────────── 其他登录 ──────────────│
|
||||
│ [手机验证码登录 - 即将开放] │ ← 禁用态,灰色
|
||||
│ [微信扫码登录 - 即将开放] │ ← 禁用态,灰色
|
||||
│ │
|
||||
│ 切换公司 │ ← 底部,小字链接
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 5.2.2 安全机制
|
||||
|
||||
| 机制 | 规格 |
|
||||
|------|------|
|
||||
| 验证码类型 | **滑块拼图行为验证码**:展示带缺口的背景图 + 可拖动的拼图碎片,用户滑动碎片至缺口完成验证,无需输入字符 |
|
||||
| 验证逻辑 | 服务端综合校验**位置偏差**(缺口中心 ±5px 容差)+ **滑动轨迹特征**(加速/减速曲线、总耗时),双重判断是否为人类行为 |
|
||||
| 背景图来源 | 预置图库随机抽取,缺口位置每次服务端随机生成,防止固定模式被预测 |
|
||||
| 验证码有效期 | 单次验证会话有效,提交登录后服务端 Token 立即失效;超过 3 分钟未操作需重新加载 |
|
||||
| 验证失败处理 | 拼图区域抖动动画提示,自动刷新新背景图;**不计入账号密码错误次数**(行为验证失败属独立事件) |
|
||||
| 密码错误锁定 | 同一账号连续密码错误 ≥ 5 次,锁定 30 分钟;解锁方式:等待超时自动解锁 或 管理员手动解锁 |
|
||||
| 密码错误计数 | 计数存于 Redis,Key 格式:`login_fail:tenant_id:username`,TTL 30 分钟 |
|
||||
| 验证码刷新 | 登录失败(用户名/密码错误)后自动刷新拼图;用户亦可主动点击「刷新」图标重新加载背景图 |
|
||||
| HTTPS | 所有登录相关请求强制 HTTPS,不允许 HTTP 降级 |
|
||||
| 密码传输 | 前端不做密码加密,HTTPS 层保证传输安全;后端存储使用 `django.contrib.auth` 默认的 `PBKDF2+SHA256` 哈希 |
|
||||
| Session 有效期 | 默认 8 小时(工作日单日使用场景);可由租户管理员在「系统设置」中调整 |
|
||||
|
||||
---
|
||||
|
||||
### 5.3 账号与员工实名绑定规范
|
||||
|
||||
#### 5.3.1 绑定原则
|
||||
|
||||
- 每个系统登录账号(username)必须与「组织人事管理」模块中的一条**员工档案(Staff)**绑定
|
||||
- 员工档案包含:姓名、工号、手机号(可选,未来用于手机登录)、邮箱(用于找回密码)、所属门店/组别
|
||||
- 账号与员工是 **1:1 关系**,一个员工对应一个账号,一个账号只能绑定一个员工
|
||||
- 账号创建流程:**由系统管理员在「组织人事管理 → 员工列表」中为员工开通账号**,不支持用户自行注册
|
||||
- 账号禁用:员工离职或被停用时,对应账号自动禁用;禁用账号无法登录,但历史操作记录保留
|
||||
|
||||
#### 5.3.2 账号字段规范
|
||||
|
||||
| 字段 | 规格 | 说明 |
|
||||
|------|------|------|
|
||||
| 用户名(username) | 英文字母开头,仅包含字母/数字/下划线,6~30 字符,同租户内唯一 | 登录 ID,不可更改 |
|
||||
| 密码(password) | 8~32 位,含字母+数字,建议含特殊字符 | 使用 `PBKDF2+SHA256` 哈希存储 |
|
||||
| 邮箱(email) | 标准邮箱格式,同租户内唯一(可选,但若空则无法自助找回密码) | 用于找回账号/密码 |
|
||||
| 手机号(phone) | 中国大陆 11 位手机号,加密存储,同租户内唯一(可选) | 预留,v2 用于手机登录 |
|
||||
| 员工档案关联(staff_id) | 外键关联 `org.Staff` 模型 | 实名绑定 |
|
||||
| 账号状态(status) | `active` / `disabled` / `locked` | locked 为密码错误锁定,30 分钟自动恢复 |
|
||||
| 初始密码 | 由管理员创建账号时设置或系统随机生成 | 建议首次登录强制修改密码 |
|
||||
|
||||
#### 5.3.3 首次登录强制修改密码
|
||||
|
||||
- 管理员新建账号或重置密码后,账号处于「初始密码」状态
|
||||
- 持有「初始密码」的账号登录成功后,系统自动弹出「请修改初始密码」强制页面,**不可跳过**
|
||||
- 修改成功后,账号状态更新为正常,进入系统首页
|
||||
|
||||
---
|
||||
|
||||
### 5.4 找回流程详细说明
|
||||
|
||||
#### 5.4.1 找回用户名流程
|
||||
|
||||
```
|
||||
用户点击「忘记用户名」
|
||||
│
|
||||
├─ 输入邮箱地址
|
||||
│ │
|
||||
│ 服务端查询该邮箱是否绑定账号(不向前端返回查询结果,防止枚举)
|
||||
│ │
|
||||
│ 统一响应「如该邮箱已绑定账号,您将收到邮件」
|
||||
│ │
|
||||
│ 后台:若邮箱存在 → 发送邮件(包含用户名)
|
||||
│ 若邮箱不存在 → 静默处理,不发送
|
||||
│
|
||||
└─ 用户查收邮件,获取用户名 → 返回登录
|
||||
```
|
||||
|
||||
**邮件模板(找回用户名)**:
|
||||
|
||||
```
|
||||
主题:您的 Fonrey 房睿用户名
|
||||
|
||||
您好,
|
||||
|
||||
您请求找回在 [公司名称] 的 Fonrey 账号用户名。
|
||||
|
||||
您的用户名为:{username}
|
||||
|
||||
如果这不是您的操作,请忽略此邮件。如有疑问,请联系您的系统管理员。
|
||||
|
||||
此邮件由系统自动发送,请勿回复。
|
||||
发送时间:{datetime}
|
||||
```
|
||||
|
||||
#### 5.4.2 找回密码流程
|
||||
|
||||
```
|
||||
用户点击「忘记密码」
|
||||
│
|
||||
步骤1:输入用户名 + 邮箱
|
||||
│
|
||||
├─ 服务端校验(用户名 + 邮箱匹配)
|
||||
│ │
|
||||
│ 统一响应「如信息匹配,重置链接将发送至您的邮箱」
|
||||
│ │
|
||||
│ 后台:匹配成功 → 生成加密 Token(含过期时间 30min)→ 发送邮件
|
||||
│ 不匹配 → 静默处理
|
||||
│
|
||||
步骤2:用户点击邮件中的重置链接
|
||||
│
|
||||
├─ 服务端校验 Token 有效性
|
||||
│ │
|
||||
│ 有效 → 展示「重置密码」表单
|
||||
│ │
|
||||
│ 无效/过期 → 提示「链接已过期,请重新申请」
|
||||
│
|
||||
步骤3:用户输入并提交新密码
|
||||
│
|
||||
├─ 密码复杂度校验
|
||||
├─ 与历史密码对比校验(最近 3 次)
|
||||
│
|
||||
└─ 校验通过 → 更新密码 → 清除所有 Session → 跳转登录界面
|
||||
```
|
||||
|
||||
**邮件模板(重置密码)**:
|
||||
|
||||
```
|
||||
主题:重置您的 Fonrey 房睿密码
|
||||
|
||||
您好,
|
||||
|
||||
我们收到了重置您在 [公司名称] 的 Fonrey 账号密码的请求。
|
||||
|
||||
请点击以下链接重置密码(链接 30 分钟内有效):
|
||||
{reset_link}
|
||||
|
||||
如果您未发起此请求,请忽略此邮件,您的密码不会被更改。
|
||||
|
||||
此邮件由系统自动发送,请勿回复。
|
||||
发送时间:{datetime}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.5 后端数据模型设计
|
||||
|
||||
#### 5.5.1 `auth` App 目录结构
|
||||
|
||||
在现有 `fonrey/apps/` 目录下新增(或扩展 Django `auth` 系统):
|
||||
|
||||
```
|
||||
apps/
|
||||
└── accounts/ # 账号认证管理(租户级 App)
|
||||
├── models.py # UserAccount, LoginAttempt, PasswordResetToken
|
||||
├── views.py # 登录/登出/找回账号/找回密码视图
|
||||
├── urls.py
|
||||
├── serializers.py # API 序列化(如需 JSON 接口)
|
||||
└── services/
|
||||
├── auth.py # 认证逻辑(验证码校验、账号锁定判断)
|
||||
├── recovery.py # 找回密码/用户名逻辑
|
||||
└── tenant.py # Tenant 验证逻辑(属于 shared_apps)
|
||||
```
|
||||
|
||||
#### 5.5.2 `UserAccount` 核心字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | BigAutoField | 主键 |
|
||||
| `username` | CharField(30) | 登录名,同租户唯一,不可变更 |
|
||||
| `password` | CharField | PBKDF2+SHA256 哈希,使用 Django `make_password` |
|
||||
| `email` | EmailField | 绑定邮箱,同租户唯一,选填 |
|
||||
| `phone` | CharField(11) | 绑定手机号,加密存储(`core.encryption`),选填 |
|
||||
| `staff` | OneToOneField → `org.Staff` | 员工档案绑定,必须 |
|
||||
| `status` | CharField | `active` / `disabled` / `locked` |
|
||||
| `is_initial_password` | BooleanField | True 时登录后强制修改密码 |
|
||||
| `last_login` | DateTimeField | 最后登录时间 |
|
||||
| `created_at` | DateTimeField | 账号创建时间 |
|
||||
| `created_by` | ForeignKey → self | 创建人(管理员) |
|
||||
|
||||
#### 5.5.3 `LoginAttempt` 登录尝试记录
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `username` | CharField | 尝试登录的用户名 |
|
||||
| `ip_address` | GenericIPAddressField | 来源 IP |
|
||||
| `success` | BooleanField | 是否成功 |
|
||||
| `failure_reason` | CharField | `wrong_password` / `wrong_captcha` / `account_locked` 等 |
|
||||
| `attempted_at` | DateTimeField | 尝试时间 |
|
||||
|
||||
> **注意**:`LoginAttempt` 属于合规审计数据,保留周期建议 ≥ 90 天。
|
||||
|
||||
#### 5.5.4 `PasswordResetToken`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `user` | ForeignKey → `UserAccount` | 关联账号 |
|
||||
| `token` | CharField(64) | 加密 Token(`secrets.token_urlsafe(32)`) |
|
||||
| `expires_at` | DateTimeField | 过期时间(创建时间 + 30 分钟) |
|
||||
| `is_used` | BooleanField | 是否已使用(使用后立即标记为 True) |
|
||||
| `created_at` | DateTimeField | 创建时间 |
|
||||
|
||||
---
|
||||
|
||||
### 5.6 Electron 客户端登录相关约定
|
||||
|
||||
| 约定项 | 规格 |
|
||||
|--------|------|
|
||||
| Tenant ID 存储 | `electron-store` 或 `app.getPath('userData')` + AES 加密,不存储明文 |
|
||||
| Session Token 存储 | 内存(`global` 变量)+ `session` Cookie(Chromium 管理),不写入磁盘明文文件 |
|
||||
| 登录页加载 | 客户端主进程根据 Tenant ID 构建目标 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` 属于租户级 Schema,Tenant 验证接口属于 `shared_apps` |
|
||||
| `Redis` | 滑块验证 Token 存储、登录失败计数、密码重置 Token 缓存 | 验证 Key:`captcha_token:{uuid}`(TTL 3min);登录失败 Key:`login_fail:{tenant_id}:{username}` |
|
||||
| `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 ID 枚举攻击(暴力试探) | 低 | 中 | Tenant 验证接口限流(每IP每分钟≤10次),返回结果不区分「未找到」与「已禁用」|
|
||||
| 密码重置 Token 泄露 | 低 | 高 | Token 单次有效、30分钟过期、HTTPS 传输 |
|
||||
| 邮件发送失败导致用户无法找回密码 | 中 | 中 | 邮件发送失败写入告警日志,管理员可通过后台查看 Token 手动告知用户 |
|
||||
| 多端同时登录同一账号 | 高(日常场景) | 低 | 本期允许,后续如需踢出,可在 Token 机制中引入版本号 |
|
||||
|
||||
### 6.4 开放问题(开发前需确认)
|
||||
|
||||
- [ ] **邮件服务商选型**:使用 SendGrid / 阿里云邮件推送 / SMTP 自建?需运维确认 — 负责人:后端负责人 — 截止:开发启动前
|
||||
- [ ] **Session 有效期默认值**:8 小时是否满足各租户需求?是否允许租户管理员自行配置?— 负责人:产品经理 — 截止:开发启动前
|
||||
- [ ] **滑块拼图实现方案**:自研(Pillow 生成图片 + 前端拖拽组件)还是集成第三方行为验证服务(如极验 GeeTest / 网易易盾)?自研可控但需维护图库;第三方开箱即用但引入外部依赖,需评估数据合规要求 — 负责人:后端负责人 + 安全 — 截止:开发启动前
|
||||
- [ ] **账号锁定通知**:账号被锁定后,是否自动发邮件通知用户和/或管理员?— 负责人:产品经理 — 截止:开发启动前
|
||||
- [ ] **历史密码校验范围**:最近 3 次是否足够?是否需要额外规则(如不能与用户名相同)?— 负责人:产品经理 — 截止:开发启动前
|
||||
|
||||
---
|
||||
|
||||
## 7. 发布计划
|
||||
|
||||
| 阶段 | 时间 | 受众 | 准入门槛 |
|
||||
|------|------|------|---------|
|
||||
| 内部 Alpha | 待定 | 研发团队 + 1 家种子租户 | 核心流程(Tenant 识别 + 账密登录 + 找回密码)无 P0 Bug |
|
||||
| 封闭 Beta | 待定 | 5 ~ 10 家测试租户 | 登录成功率 ≥ 99%,验证码拦截机制正常运作 |
|
||||
| 正式发布 | 待定 | 全量租户 | Beta 阶段无未修复的安全漏洞;帮助文档发布 |
|
||||
|
||||
**回滚标准**:若正式发布后 24 小时内登录失败率(非验证码拦截原因)超过 2%,或出现账号数据泄露事件,立即回滚并启动安全审查。
|
||||
|
||||
---
|
||||
|
||||
## 8. 附录
|
||||
|
||||
### 8.1 登录状态流转图
|
||||
|
||||
```
|
||||
[未识别 Tenant]
|
||||
│ 输入有效 Tenant ID
|
||||
↓
|
||||
[未登录]
|
||||
│ 账密登录成功
|
||||
↓
|
||||
[初始密码状态](如账号为初始密码)
|
||||
│ 强制修改密码成功
|
||||
↓
|
||||
[已登录 - Active Session]
|
||||
│ Session 过期 / 主动登出 / 管理员强制登出
|
||||
↓
|
||||
[未登录](跳转登录界面)
|
||||
|
||||
[账号锁定状态](5次错误后)
|
||||
│ 30 分钟后自动解锁 或 管理员手动解锁
|
||||
↓
|
||||
[未登录](可重新登录)
|
||||
```
|
||||
|
||||
### 8.2 接口清单汇总
|
||||
|
||||
| 接口 | 方法 | Schema 位置 | 是否需要鉴权 | 说明 |
|
||||
|------|------|------------|------------|------|
|
||||
| `/api/auth/tenant/verify/` | POST | Public(shared) | 否 | Tenant ID 验证 |
|
||||
| `/api/auth/captcha/` | GET | Tenant | 否 | 获取滑块拼图验证码(返回背景图 Base64 + 碎片图 Base64 + 验证 Token) |
|
||||
| `/api/auth/captcha/verify/` | POST | Tenant | 否 | 提交滑动轨迹 + 位置,服务端校验并返回一次性通过凭证(供登录接口使用) |
|
||||
| `/api/auth/login/` | POST | Tenant | 否 | 账号密码登录 |
|
||||
| `/api/auth/logout/` | POST | Tenant | 是 | 登出,使 Session 失效 |
|
||||
| `/api/auth/recover/username/` | POST | Tenant | 否 | 发起找回用户名 |
|
||||
| `/api/auth/recover/password/request/` | POST | Tenant | 否 | 发起找回密码(发送邮件) |
|
||||
| `/api/auth/recover/password/reset/` | POST | Tenant | 否(Token 鉴权) | 提交新密码 |
|
||||
| `/api/auth/login/phone/` | POST | Tenant | 否 | **预留**,手机验证码登录 |
|
||||
| `/api/auth/wechat/qrcode/` | GET | Tenant | 否 | **预留**,获取微信二维码 |
|
||||
| `/api/auth/wechat/callback/` | POST | Tenant | 否 | **预留**,微信扫码回调 |
|
||||
|
||||
### 8.3 相关文档参考
|
||||
|
||||
- 客户端发布管理模块 PRD:`Project/fonrey/PRD/发布管理/客户端发布管理模块PRD.md`
|
||||
- 组织人事管理模块 PRD:`Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md`
|
||||
- 权限管理模块 PRD:`Project/fonrey/PRD/权限管理/权限管理模块PRD.md`
|
||||
- 系统管理模块 PRD:`Project/fonrey/PRD/系统管理/系统管理模块PRD.md`
|
||||
- 技术栈文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
|
||||
Reference in New Issue
Block a user