Files
caiwuzongzhang/docs/decisions/003-auth.md
腾讯WorkBuddy 7f1a93f6a6 B-64: 安全修复——随机初始密码与跨公司重复上传隔离
- 公司账号创建改为随机一次性初始密码,只在创建响应中显示一次,
  密码保证不等于用户名;删除「用户名即初始密码」兼容分支,继续
  强制首次登录改密。
- 跨公司相同字节文件上传只返回通用重复状态:不再返回其他公司的
  原批次 ID、银行、模板、期间、交易数或诊断;同公司重复上传的
  幂等摘要保持可用。
- 补充服务端回归测试,覆盖同公司与跨公司两个分支及随机密码;
  完整测试 85 项全绿,node --check 通过。
2026-08-16 01:50:42 +08:00

67 lines
3.7 KiB
Markdown
Raw Permalink 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.
# 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`;首次登录必须改密,改密前所有业务 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、时间。
- 初始密码、新旧密码均不进入审计内容。