B-40: 正式登录与租户隔离——认证、会话、权限与审计

- 管理员/公司两类角色:PBKDF2 密码散列、随机初始密码、首次改密、
  停用、重置密码与吊销会话。
- 会话只存令牌摘要,8 小时绝对过期;登录失败同 (账号,IP) 限流。
- 公司账号服务端绑定唯一公司;读取/上传/导出/主数据/审核接口逐项
  服务端授权,跨公司访问返回 404 而非 403。
- 审计日志记录登录、改密、上传、导出、建公司、建/停/启用户。
- 决策记录见 docs/decisions/003-auth.md。
This commit is contained in:
腾讯WorkBuddy
2026-08-16 01:48:58 +08:00
parent 763a940683
commit f7aa4a8d06
4 changed files with 1146 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
# 003 身份认证与租户隔离技术决策
对应 IssueB-40`docs/issues/003-p0-auth-and-tenant-isolation.md`)。
## 密码散列:标准库 PBKDF2-HMAC-SHA256
- 只使用 Python 标准库(`hashlib.pbkdf2_hmac` + `hmac.compare_digest` +
`secrets`),不引入 bcrypt/argon2 等第三方依赖,与离线内网部署约束一致。
- 260 000 次迭代、16 字节随机盐,存储格式
`pbkdf2_sha256$<迭代数>$<盐hex>$<摘要hex>`,自描述、可平滑升级参数。
- 明文密码永不入库、永不进日志、永不写入 `audit_log`
`password_hash` 字段从不出现在任何 API 响应中(有测试断言)。
## 会话:数据库保存令牌摘要,8 小时绝对过期
- 令牌为 `secrets.token_urlsafe(32)`,数据库只存其 SHA-256 摘要;
数据库泄露不直接暴露可用令牌。
- 绝对过期 8 小时(一个工作班次),不做滑动续期,语义简单可测。
- 会话可吊销:退出登录、停用账号、重置密码都会立即吊销该用户全部会话。
- Cookie 名 `cw_session``HttpOnly; SameSite=Lax; Path=/`
当前是纯 HTTP 的局域网部署,**刻意不加 `Secure`**(加了浏览器会直接拒发);
若未来上 HTTPS,应补上 `Secure` 并配置反向代理。
## 登录限流:同一 (账号, IP) 10 分钟内失败 5 次即锁定
- 计数来自 `login_attempts` 表,窗口为滚动 10 分钟;触发后返回 429,
且在窗口内不再记录新尝试,行为确定、可测试。
- 失败提示统一为「账号或密码不正确」,不泄露是哪一部分错误。
## 角色与公司绑定
- `users.role``admin` / `company`;数据库 CHECK 约束强制
公司账号必须绑定公司、管理员不得绑定公司。
- 登录时前端选择工作端口(portal),服务端校验 portal 与角色一致,
不匹配返回 403「账号与该工作端口不匹配」。
- 管理员创建公司账号时,初始密码等于登录账号本身并置
`must_change_password=1`2026-08-08 产品决定,替代随机初始密码);
首次登录必须改密,改密前所有业务 API 返回 403。初始密码未改前
等同账号名,因此创建用户的审计 detail 只记录公司,不重复账号名。
- 「重置密码」仍生成随机一次性密码:重置发生在用户已改密之后,
可预测的口令会让知道账号名的人直接接管账户。
- 已知风险:初始密码可预测,账号创建后应尽快完成首登改密;
创建到改密之间,知道账号名的人即可登录该账号。
## 租户隔离在服务端强制,404 优于 403
- 公司用户的批次列表、行明细、CSV 导出、上传归属全部由服务端按
会话中的 `company_id` 过滤;请求体/参数里的 `company_id` 对公司用户
一律忽略(跨公司上传防护)。
- 访问他公司批次返回 404 而非 403:不暴露「该批次存在但属于别人」这一事实,
避免 IDOR 探测。导出时显式指定他公司 `company_id` 仍返回 403
因为用户已声明知道该公司存在,此时给出明确拒绝更有操作性。
- `GET /admin.html``/company.html` 的 302 跳转只是 UX 层引导,
不是权限边界;真正的边界全部在 API 上。
## 引导管理员(bootstrap
- 服务启动迁移后若无任何 admin 账号,创建 `APP_ADMIN_USERNAME`
(默认 `group-admin`);密码取 `APP_BOOTSTRAP_ADMIN_PASSWORD`
未设置则生成随机初始密码并**只打印一次到 stdout**(不写日志文件),
`must_change_password=1`
- 不在迁移或代码中预置任何公司或用户;公司与公司账号全部由管理员
通过 `/api/admin/companies``/api/admin/users` 动态创建。
## 审计
- `audit_log` 记录登录成功/失败、退出、改密、上传、导出(含公司范围与行数)、
建公司、建/停/启用户、重置密码等动作,含操作者、目标、IP、时间。
- 初始密码、新旧密码均不进入审计内容。