fix(密码): 五个写入点统一走 hashPassword,默认切 argon2
## 那道「单向门」其实一直开着
`PASSWORD_HASH_UPGRADE` 是并行试跑第一天撞出来的补丁:新后端把 Django 的
pbkdf2 升级成 argon2 之后,旧后端验不了,登录过新站的学生回滚就登不上
(phase5 切换手册里记着这件事)。当时的修法是给**登录时的自动升级**加个开关,
默认关闭。
但写密码的地方有五个,开关只管住了一个:
POST /users 注册
PUT /admin/users/:id 管理员改密码
POST /admin/users 批量导入用户
POST /admin/users/:id/reset-password 重置密码 ← 老师天天在用
登录成功后的自动升级 ← 只有这一处受开关管
后面四处无条件 `Bun.password.hash(argon2id)`。也就是说开关关得好好的,
**老师给学生点一次「重置密码」,那个账号就回不去旧站了** —— 而「老师帮学生
查/改密码」正是这套系统的日常功能,`raw_password` 那一列存在的理由就是它。
所以那半年里「默认关闭 = 回滚安全」是个假象。
## 改法
五处统一走 `auth/password.ts` 的 `hashPassword()`,开关两个方向都变成真的:
- `true`:五处全写 argon2id,登录时把存量 pbkdf2 顺手升级掉。
- `false`:五处全写 Django 格式的 `pbkdf2_sha256$1200000$<22位salt>$<base64>`,
登录时也不动存量哈希 —— 两边都验得了。
新加的 `hashDjangoPbkdf2` 逐项对齐 Django 6 的 `PBKDF2PasswordHasher`:
1200000 迭代、22 位 salt、`RANDOM_STRING_CHARS` 字符集、sha256/32 字节。
**默认值定成 `true`** —— 旧站今天下线了,没有回滚路径要照顾。要把旧站拉回来
就先设 `PASSWORD_HASH_UPGRADE=false`,切换手册三处说明都改了。
⚠️ `verifyPassword` 的 pbkdf2 分支**永远不能删**,代码和注释里都写了:生产库
1710 个账号全是 Django 写的 pbkdf2(迭代次数 120000~1200000,跨了好几个
Django 版本),它们只会在各自下次登录时才升级成 argon2。
## 顺带:dev seed 只有学生号
`seed:dev` 只建 `student`(普通用户),本机想测后台得手工往库里塞 email 和
user_profile —— 而缺 user_profile 的表现极其隐蔽:`/api/me` 返回
profile-not-found → 前端 getMyProfile 抛异常 → localStorage 的 authed 存不进去
→ **所有 /admin 路由被守卫静默弹回首页**,不报错。我在这上面卡了很久。
现在 seed 同时建学生和超管(`devadmin` / `devadmin123`),profile 和 email 一起
建好,并且写密码也走 hashPassword。另外加了一道防呆:DATABASE_URL 不是本机时
直接拒绝执行 —— 这个脚本会重置密码并把明文写进 raw_password,其中一个还是超管,
对着生产库跑一次就是把超管密码改掉。要绕过设 `OJ2_SEED_FORCE=true`。
## 验证
全程拿**旧后端那个真的 Django venv** 对打,不是照着文档推:
- 事实核对:Bun 写的 `$argon2id$…` 在 Django 里 `identify_hasher` 抛
`Unknown password hashing algorithm ''`;换成 Django 格式 `argon2$argon2id$…`
能识别,但 `verify()` 抛 `Couldn't load 'Argon2PasswordHasher' algorithm
library: No module named 'argon2'`(旧后端确实没装 argon2-cffi)。
原注释说的「格式对了也验不了」结论对,机制略有出入。
- 双向互验:OJ2 写的哈希 Django `check_password` 通过、错密码不通过;
Django `make_password` 写的哈希 OJ2 `verifyPassword` 也认。
- 默认(argon2):拿 Django 现造一个 120000 迭代的老哈希塞进库,登录 200 →
哈希变成 argon2 → 再登一次仍 200。
- 退路(`=false`):同一个老哈希登录 200 且**哈希不变**;走后台「重置密码」
之后落库是 `pbkdf2_sha256$1200000$…`,**旧站的 Django 验这个新密码通过**。
- 一次 1200000 迭代约 107ms(写密码时的开销;验旧哈希的开销本来就在)。
- tsc(apps/api) 0 error、check:routes 168 条无遮蔽、vue-tsc 0 error、build 通过。
测试用户(pwtest1 / legacyuser)已删干净,本机库用新 seed 复位。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -51,8 +51,22 @@ pg_dumpall 备份带 30 条 `setval`,而且直接查生产快照里所有序
|
||||
> 于是「登录过新站的学生,回滚之后登不上旧站」—— 一道结构比对发现不了的单向门。
|
||||
> 试跑第一天就撞上了,现象是旧站登录失败 + WS 连不上(Channels 认 session)。
|
||||
>
|
||||
> 已改成 `PASSWORD_HASH_UPGRADE` 控制、**默认关闭**。
|
||||
> **回滚窗口内不要打开它**,确定不会再回滚了再说。
|
||||
> 已改成 `PASSWORD_HASH_UPGRADE` 控制。
|
||||
>
|
||||
> **⚠️ 2026-08-26 再补两件事。**
|
||||
>
|
||||
> **一、当时那次只堵住了登录那一处,门其实一直开着。** 新后端一共五个地方写密码:
|
||||
> 登录时自动升级、注册、管理员改密码、批量导入用户、**重置密码**。
|
||||
> `PASSWORD_HASH_UPGRADE` 只管住了第一处,后面四处无条件写 argon2 —— 也就是说
|
||||
> 开关关着的时候,**老师给学生点一次「重置密码」,那个账号照样回不去旧站**,
|
||||
> 而这恰恰是老师天天在用的功能。现在五处统一走 `auth/password.ts` 的
|
||||
> `hashPassword()`,开关两个方向都是真的。
|
||||
>
|
||||
> **二、旧站已经下线,这个开关的默认值翻成了 `true`。** 也就是默认写 argon2、
|
||||
> 登录时升级存量哈希。**要把旧站拉回来的话,先设 `PASSWORD_HASH_UPGRADE=false`**,
|
||||
> 之后新写的密码就又是 Django 格式的 `pbkdf2_sha256$1200000$…` 了(已用旧后端的
|
||||
> Django 实打双向验过:新后端写的 `check_password` 通过,Django 写的新后端也认)。
|
||||
> 已经被升成 argon2 的存量账号仍然要按下面「万一已经改坏了」那节修。
|
||||
|
||||
---
|
||||
|
||||
@@ -297,9 +311,12 @@ WebSocket 那个开关是双跑最容易漏的一格:漏了的话页面一切
|
||||
|
||||
- **两边登录态不互通**。旧站是 Django session,新站是 Redis opaque token。
|
||||
学生到 oj2 要重新登录一次,这不是 bug。
|
||||
- **`PASSWORD_HASH_UPGRADE` 必须关着**(默认就是关的,别手贱打开)。打开的话,
|
||||
在 oj2 登录过的账号会被改成 argon2 哈希,**回旧站就登不上了** ——
|
||||
试跑第一天真撞过,现象是旧站登录失败 + WS 连不上。修复见下面的「万一已经改坏了」。
|
||||
- **想保住回滚路径就得设 `PASSWORD_HASH_UPGRADE=false`**(2026-08-26 起默认是
|
||||
`true`,因为旧站已经下线)。默认值下,在 oj2 登录过或被改过密码的账号会变成
|
||||
argon2 哈希,**回旧站就登不上了** —— 试跑第一天真撞过,现象是旧站登录失败 +
|
||||
WS 连不上。修复见下面的「万一已经改坏了」。设成 `false` 之后是真安全:注册 /
|
||||
改密码 / 重置密码 / 批量导入写的也都是 Django 格式的 pbkdf2(以前这几条路
|
||||
无视开关直接写 argon2)。
|
||||
- **同一个库,双写**。结构完全兼容不会写坏,但提交、统计、成就都是**真实数据**,
|
||||
不是沙盒。别拿它做破坏性试验。
|
||||
- 后台判题机列表会出现**两台**(新旧各自心跳),正常。
|
||||
@@ -426,10 +443,14 @@ backend 和判题机再 start 起来。**不需要恢复数据库,不需要动
|
||||
这也是「只换前后端」形态的主要好处:切换和回滚都不碰数据库进程,
|
||||
库出问题的可能性从流程里被整个拿掉了。
|
||||
|
||||
### ⚠️ 回滚成立的前提:`PASSWORD_HASH_UPGRADE` 关着
|
||||
### ⚠️ 回滚成立的前提:`PASSWORD_HASH_UPGRADE=false`
|
||||
|
||||
「不动任何数据」只在这个前提下成立。打开它的话,新站登录过的账号密码哈希会被换成
|
||||
argon2,旧后端验不了 —— 回滚之后那些学生登不上。**默认是关的,回滚窗口内别打开。**
|
||||
「不动任何数据」只在这个前提下成立。默认值(`true`,2026-08-26 旧站下线后改的)
|
||||
下,新站登录过、注册的、被老师改过或重置过密码的账号,哈希都会变成 argon2,
|
||||
旧后端验不了 —— 回滚之后那些学生登不上。
|
||||
|
||||
**真要留回滚路径,先把 `PASSWORD_HASH_UPGRADE=false` 设上再说**,已经变成
|
||||
argon2 的按下面那节修。
|
||||
|
||||
### 万一已经改坏了
|
||||
|
||||
|
||||
Reference in New Issue
Block a user