15 KiB
15 KiB
For AI assistants: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked.
Fonrey 登录管理系统技术方案
版本: 1.0 | 项目: Fonrey 房产经纪管理系统 | 技术栈: Django 4.x + HTMX + Alpine.js + PostgreSQL + Redis + Celery
关联 PRD: Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md (v1.3)
For AI assistants: Read this entire file before writing any code.
All decisions here are final. Do not suggest alternatives unless asked.
一、模块定位与架构边界
登录管理模块(accounts App)负责多租户环境下的身份识别、认证、账号安全及凭据找回。其架构边界如下:
| 层级 | 位置 | 说明 |
|---|---|---|
| Tenant ID 验证 | shared_apps(公共 Schema) |
属于平台基础服务,在 public schema 下运行,无需租户切换 |
| 账号认证、找回密码等 | 租户 Schema(Tenant Schema) | 通过请求域名 {tenant_slug}.fonrey.com 自动切换,django-tenants 中间件处理 |
| Electron 客户端 | 前端 | 负责 Tenant ID 本地缓存、Session Token 管理、页面加载 |
二、依赖与技术选型
| 依赖项 | 版本/方案 | 用途 | 说明 |
|---|---|---|---|
django.contrib.auth |
Django 内置 | 用户认证基础框架 | 扩展 AbstractBaseUser,不直接使用 User 模型;username 唯一性约束在租户 Schema 维度生效,而非全局 |
django-tenants |
已有 | 多租户隔离 | UserAccount 在租户 Schema;Tenant 验证接口在 shared_apps |
PostgreSQL |
已有 | 数据持久化 | Schema 级别隔离租户数据 |
Redis |
必须 | 多用途缓存 | 滑块验证 Token(TTL 3min)、登录失败计数(TTL 30min)、密码重置 Token 缓存 |
Celery |
必须 | 异步任务队列 | 邮件发送异步处理,防止登录/找回接口超时 |
Pillow |
必须(若自研验证码) | 图片处理 | 生成拼图背景图(抠出缺口)+ 拼图碎片,输出 Base64 |
django-ratelimit 或自定义中间件 |
必须 | 接口限流 | Tenant 验证、登录、找回密码接口均需限流 |
electron-store 或 AES 加密文件 |
Electron 侧 | 本地持久化 | 加密存储 Tenant ID,不存明文;路径为 app.getPath('userData') |
secrets (Python 标准库) |
Python 内置 | Token 生成 | 使用 secrets.token_urlsafe(32) 生成密码重置 Token |
滑块验证码方案选型(待确认,见开放问题)
| 方案 | 优点 | 缺点 |
|---|---|---|
| 自研(Pillow + 前端拖拽组件) | 完全可控,无外部依赖,数据合规性好 | 需维护图库,需自己实现轨迹检测算法 |
| 第三方服务(极验 GeeTest / 网易易盾) | 开箱即用,安全性更高 | 引入外部依赖,有数据合规风险,需评估 |
当前方案:暂按自研设计,后端负责人需在开发启动前确认最终选型。
三、目录结构
fonrey/apps/
└── accounts/ # 账号认证管理(租户级 App)
├── models.py # UserAccount, LoginAttempt, PasswordResetToken
├── views.py # 登录/登出/找回账号/找回密码视图
├── urls.py
├── serializers.py # API 序列化(JSON 接口)
└── services/
├── auth.py # 认证逻辑(验证码校验、账号锁定判断)
├── recovery.py # 找回密码/用户名逻辑(含邮件发送 Celery 任务)
└── tenant.py # Tenant 验证逻辑(属于 shared_apps,公共 Schema)
四、数据模型
4.1 UserAccount(核心账号表,位于租户 Schema)
class UserAccount(AbstractBaseUser):
id = BigAutoField(primary_key=True)
username = CharField(max_length=30) # 同租户内唯一;普通员工为手机号;Tenant Admin 为自定义字符串
email = EmailField(null=True, blank=True) # 同租户唯一,为空则无法自助找回密码
phone = CharField(max_length=11, null=True) # 加密存储(core.encryption);普通员工必填
staff = OneToOneField('org.Staff', null=True, on_delete=SET_NULL) # 实名绑定;普通员工必须
is_tenant_admin = BooleanField(default=False)
status = CharField(max_length=10) # active / disabled / locked
is_initial_password = BooleanField(default=True) # True → 登录后强制跳转修改密码
last_login = DateTimeField(null=True)
created_at = DateTimeField(auto_now_add=True)
created_by = ForeignKey('self', null=True, on_delete=SET_NULL)
USERNAME_FIELD = 'username'
class Meta:
unique_together = [('username',)] # Schema 内唯一,跨租户不冲突
关键约束:
username唯一性约束仅在当前租户 Schema 内生效(django-tenants隔离机制),不同租户可以有相同 username- 密码存储使用 Django 默认
PBKDF2+SHA256(make_password),后端不得明文存储或传输 phone字段使用core.encryption加密存储
4.2 LoginAttempt(登录审计,位于租户 Schema)
class LoginAttempt(Model):
username = CharField(max_length=30)
ip_address = GenericIPAddressField()
success = BooleanField()
failure_reason = CharField(max_length=30, null=True)
# 可选值:wrong_password / wrong_captcha / account_locked / account_disabled
attempted_at = DateTimeField(auto_now_add=True)
保留策略:合规审计数据,最少保留 90 天,不得提前清理。
4.3 PasswordResetToken(密码重置令牌,位于租户 Schema)
class PasswordResetToken(Model):
user = ForeignKey(UserAccount, on_delete=CASCADE)
token = CharField(max_length=64) # secrets.token_urlsafe(32) 生成
expires_at = DateTimeField() # created_at + 30 分钟
is_used = BooleanField(default=False)
created_at = DateTimeField(auto_now_add=True)
安全约束:
- Token 单次有效(使用后立即设
is_used=True) - 有效期 30 分钟,过期后拒绝使用
- 同一账号 1 小时内最多生成 3 个有效 Token(服务端计数)
五、Redis Key 规范
| 用途 | Key 格式 | TTL | 说明 |
|---|---|---|---|
| 滑块验证会话 Token | captcha_token:{uuid} |
3 分钟 | 前端拖动完成后服务端生成一次性通过凭证 |
| 登录失败计数 | login_fail:{tenant_id}:{username} |
30 分钟 | 计数 ≥ 5 时锁定账号;TTL 30 分钟自动解锁 |
| 找回邮件发送频率 | recover_email:{email} |
1 小时 | 记录已发送次数,上限 3 次/小时 |
| Tenant ID 限流 | tenant_verify_ip:{ip} |
1 分钟 | 计数 ≥ 10 时拒绝请求 |
六、接口清单
| 接口 | 方法 | Schema 位置 | 是否需要鉴权 | 限流规则 | 说明 |
|---|---|---|---|---|---|
/api/auth/tenant/verify/ |
POST | Public(shared) | 否 | 每 IP 每分钟 ≤ 10 次 | Tenant ID 验证 |
/api/auth/captcha/ |
GET | Tenant | 否 | — | 获取滑块拼图验证码(背景图 Base64 + 碎片图 Base64 + 验证 Token) |
/api/auth/captcha/verify/ |
POST | Tenant | 否 | — | 提交滑动轨迹 + 位置,返回一次性通过凭证 |
/api/auth/login/ |
POST | Tenant | 否 | 每 IP 每分钟 ≤ 20 次 | 账号密码登录 |
/api/auth/logout/ |
POST | Tenant | 是 | — | 登出,使服务端 Session 失效 |
/api/auth/recover/username/ |
POST | Tenant | 否 | 每邮箱每小时 ≤ 3 次 | 发起找回用户名(发送邮件) |
/api/auth/recover/password/request/ |
POST | Tenant | 否 | 每账号每小时 ≤ 3 次 | 发起找回密码(发送重置链接邮件) |
/api/auth/recover/password/reset/ |
POST | Tenant | 否(Token 鉴权) | — | 提交新密码,使用 PasswordResetToken 校验 |
/api/auth/login/phone/ |
POST | Tenant | 否 | — | 预留,v2 实现,手机验证码登录 |
/api/auth/wechat/qrcode/ |
GET | Tenant | 否 | — | 预留,v2 实现,获取微信二维码 |
/api/auth/wechat/callback/ |
POST | Tenant | 否 | — | 预留,v2 实现,微信扫码回调 |
Tenant 验证接口 Request/Response 规范
POST /api/auth/tenant/verify/
Request Body:
{
"tenant_id": "202500010001" // 固定 12 位纯数字
}
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": "识别码无效"
}
注意:失败响应统一返回 HTTP 200,不区分"未找到"与"已禁用",防止枚举攻击。
登录接口核心逻辑
POST /api/auth/login/
Request Body:
{
"username": "string",
"password": "string",
"captcha_token": "string", // 滑块验证通过后的一次性凭证
"captcha_pass_token": "string"
}
Response 200 (成功):
{
"token": "...",
"user": {
"id": 1,
"username": "...",
"display_name": "...",
"is_initial_password": false
}
}
七、安全机制设计
7.1 滑块拼图验证码
- 图片生成:
Pillow从预置图库随机抽取背景图,服务端随机生成缺口位置,抠出缺口并生成拼图碎片,两者分别以 Base64 返回前端 - 轨迹校验:前端记录滑动过程的坐标序列 + 时间戳,提交至
/api/auth/captcha/verify/;服务端综合校验:- 位置偏差:碎片最终位置与缺口中心偏差 ≤ ±5px
- 轨迹特征:存在加速→减速的非线性运动曲线;拒绝匀速/程序化轨迹
- 独立性:验证码失败不计入账号密码错误次数,两者独立计数
- 有效期:通过凭证(
captcha_pass_token)TTL 3 分钟,单次有效
7.2 账号锁定机制
- 同一账号(
login_fail:{tenant_id}:{username})连续密码错误 ≥ 5 次:- 账号状态置为
locked,持续 30 分钟 - Redis TTL 30 分钟到期后自动恢复,同时
status更新为active - Tenant Admin 可在管理界面手动解锁(提前恢复)
- 账号状态置为
7.3 密码安全
| 规则 | 说明 |
|---|---|
| 存储哈希 | Django PBKDF2+SHA256(make_password) |
| 传输安全 | 强制 HTTPS,前端不加密密码(HTTPS 层保证) |
| 复杂度 | 长度 8~32 位,必须包含字母(区分大小写)+ 数字;建议特殊符号(非强制) |
| 历史密码 | 不得与最近 3 次历史密码相同(含系统固定初始密码 Fonrey@2025) |
| Session 有效期 | 默认 8 小时;可由 Tenant Admin 在「系统设置」中调整 |
7.4 密码重置流程安全要点
- Token 由
secrets.token_urlsafe(32)生成,64 字符,全局唯一 - 单次有效:使用后立即标记
is_used=True - 有效期 30 分钟(
expires_at = created_at + timedelta(minutes=30)) - 重置成功后:清除该账号所有有效 Session(强制重新登录)
- 重置成功后:
is_initial_password = False
八、Electron 客户端约定
| 约定项 | 规格 |
|---|---|
| Tenant ID 存储 | electron-store 或 app.getPath('userData') + AES 加密文件,不存明文 |
| Session Token 存储 | 内存(global 变量)+ Chromium session Cookie,不写入磁盘明文文件 |
| 登录页加载方式 | 主进程根据 Tenant ID 构建 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 时,阻断登录流程,展示更新提示 |
九、多租户隔离要点
UserAccount、LoginAttempt、PasswordResetToken均位于租户 Schema 内,数据完全隔离username唯一性约束在 Schema 维度生效,不同租户可以存在相同 username- Tenant 验证接口(
/api/auth/tenant/verify/)位于 Public Schema(shared_apps),查询TenantModel - 登录等接口通过请求域名(
{tenant_slug}.fonrey.com)自动切换 Schema,由django-tenants中间件处理,无需手动切换
十、已知风险与缓解措施
| 风险 | 可能性 | 影响 | 缓解措施 |
|---|---|---|---|
| 滑块验证被机器模拟轨迹绕过 | 低 | 高 | 服务端同时校验位置偏差 + 轨迹曲线特征,拒绝匀速/程序化轨迹;后续可引入设备指纹 |
| Tenant ID 枚举攻击 | 低 | 中 | 限流(每 IP 每分钟 ≤ 10 次);响应不区分"未找到"与"已禁用" |
| 密码重置 Token 泄露 | 低 | 高 | 单次有效 + 30 分钟过期 + HTTPS 传输 |
| 邮件发送失败 | 中 | 中 | 异步任务失败写入告警日志;管理员可通过后台查看 Token 手动告知用户 |
| 多端并发登录 | 高(正常场景) | 低 | 本期允许;v2 可在 Token 引入版本号实现踢出策略 |
十一、开放问题(开发启动前必须确认)
| 问题 | 负责人 | 截止 |
|---|---|---|
| 邮件服务商选型:SendGrid / 阿里云邮件推送 / SMTP 自建? | 后端负责人 + 运维 | 开发启动前 |
| 滑块验证码方案:自研(Pillow)还是第三方(极验 / 网易易盾)? | 后端负责人 + 安全 | 开发启动前 |
| Session 有效期默认值 8 小时,是否允许 Tenant Admin 自行配置? | 产品经理 | 开发启动前 |
| 账号锁定后是否自动发邮件通知用户和/或管理员? | 产品经理 | 开发启动前 |
| 历史密码校验范围:最近 3 次是否足够?是否增加"不得与用户名相同"规则? | 产品经理 | 开发启动前 |
十二、明确禁止
- ❌ 不得使用 Django 原生
User模型,必须扩展AbstractBaseUser - ❌ 不得在全局 Schema 创建
UserAccount表(必须在租户 Schema 内) - ❌ 不得明文存储或传输密码
- ❌ 不得在
LoginAttempt记录中存储密码明文(含错误密码) - ❌ 不得在前端做密码哈希(HTTPS 层保证传输安全)
- ❌ 不得将 Session Token 写入 Electron 磁盘明文文件
- ❌ 不得在找回账号/密码响应中区分"邮箱存在"与"邮箱不存在"(防止枚举)
- ❌
PasswordResetToken不得重复使用(is_used=True后立即失效) - ❌ 登录失败响应不得区分"用户名错误"与"密码错误"