3.9 KiB
3.9 KiB
003 身份认证与租户隔离技术决策
对应 Issue:B-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, 且在窗口内不再记录新尝试,行为确定、可测试。 - 失败提示统一为「账号或密码不正确」,不泄露是哪一部分错误。
- 运维开关:环境变量
APP_LOGIN_RATE_LIMIT_DISABLED设为1/true/yes/on时完全跳过限流检查,仅限内网测试环境临时使用;默认未设置,保持 「5 次失败锁 10 分钟」的生产安全策略不变。
角色与公司绑定
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、时间。- 初始密码、新旧密码均不进入审计内容。