Files
nexus/Project/fonrey/TECH_STACK/登录管理技术方案.md

15 KiB
Raw Blame History

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 下运行,无需租户切换
账号认证、找回密码等 租户 SchemaTenant Schema 通过请求域名 {tenant_slug}.fonrey.com 自动切换,django-tenants 中间件处理
Electron 客户端 前端 负责 Tenant ID 本地缓存、Session Token 管理、页面加载

二、依赖与技术选型

依赖项 版本/方案 用途 说明
django.contrib.auth Django 内置 用户认证基础框架 扩展 AbstractBaseUser不直接使用 User 模型username 唯一性约束在租户 Schema 维度生效,而非全局
django-tenants 已有 多租户隔离 UserAccount 在租户 SchemaTenant 验证接口在 shared_apps
PostgreSQL 已有 数据持久化 Schema 级别隔离租户数据
Redis 必须 多用途缓存 滑块验证 TokenTTL 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+SHA256make_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 Publicshared 每 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_tokenTTL 3 分钟,单次有效

7.2 账号锁定机制

  • 同一账号(login_fail:{tenant_id}:{username})连续密码错误 ≥ 5 次:
    • 账号状态置为 locked,持续 30 分钟
    • Redis TTL 30 分钟到期后自动恢复,同时 status 更新为 active
    • Tenant Admin 可在管理界面手动解锁(提前恢复)

7.3 密码安全

规则 说明
存储哈希 Django PBKDF2+SHA256make_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-storeapp.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 时,阻断登录流程,展示更新提示

九、多租户隔离要点

  • UserAccountLoginAttemptPasswordResetToken 均位于租户 Schema 内,数据完全隔离
  • username 唯一性约束在 Schema 维度生效,不同租户可以存在相同 username
  • Tenant 验证接口(/api/auth/tenant/verify/)位于 Public Schemashared_apps),查询 TenantModel
  • 登录等接口通过请求域名({tenant_slug}.fonrey.com)自动切换 Schemadjango-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 后立即失效)
  • 登录失败响应不得区分"用户名错误"与"密码错误"