feat(auth): 口令强度策略(弱口令黑名单 / 账号派生值 / 长度上限)

背景(R61 实测):浏览器密码管理器在「设置密码」页预填 123456,考生若直接提交,该口令即成为其账号真实口令;而后端此前仅由 validate 校验「长度 >= 6」,弱口令完全合法。R62 已从前端修掉预填,本次补齐后端这道闸门。

- app/lib/passwordPolicy.js + password_blacklist.json:口令策略唯一事实来源。黑名单刻意做成纯 JSON,供 Node 运行时与运维脚本共用,避免两份清单漂移。

- controller/auth.js:activate() 与 resetPassword() 接入策略(错误码 2001 / 7001),位置在 validate 之后、短信校验之前(纯本地计算、无副作用、快速失败)。

- validate/index.js:补 max:20,与前端 el-form 规则对齐(此前只有前端限制长度上限)。

- scripts/_db.cjs:DB 配置解析唯一入口;scan_weak_password_accounts.cjs:存量弱口令只读扫描器;verify_password_policy_api.cjs:真接口验证(含「强口令通过」正向对照臂,防假绿)。
This commit is contained in:
2026-09-30 11:56:35 +08:00
parent 31da9580e5
commit a2e24aba2a
7 changed files with 1080 additions and 1 deletions

View File

@@ -3,6 +3,9 @@
const Controller = require('egg').Controller;
const svgCaptcha = require('svg-captcha');
const VALIDATION_RULES = require('../validate');
// 口令强度策略(唯一事实来源):黑名单 + 纯重复/序列 + 账号派生值 + 长度上下限。
// ⚠️ 任何新增的口令写入入口都必须调用它,禁止在控制器内自行判断(见 lib/passwordPolicy.js 模块头)。
const passwordPolicy = require('../lib/passwordPolicy');
/**
* 认证控制器 - AuthController
@@ -206,6 +209,21 @@ class AuthController extends Controller {
return;
}
// 口令强度校验(R63 · C1)
// 背景:此前仅由 validate 校验「长度 ≥ 6」,`123456` 完全合法 —— R61 测试人员
// 实测「浏览器密码管理器预填 123456,直接提交后即成为账号口令」。
// 位置(validate 之后 / 短信码之前)的理由:
// · validate 只管「必填 + 格式」,强度是业务规则,分层后两者失败文案互不覆盖;
// · 纯本地计算、无副作用,可快速失败,不消耗也不依赖 session 中的验证码。
// 错误码 2001 = 口令强度不足(activate 段编码;resetPassword 段用 7001)。
// ⚠️ 日志只记命中的规则名,绝不记录口令明文或身份证号。
const pwdCheck = passwordPolicy.checkPassword(password, { idNumber: id_number, phone });
if (!pwdCheck.ok) {
ctx.logger.info(`[口令策略] 激活口令被拒:rule=${pwdCheck.rule}`);
ctx.body = ctx.helper.error(2001, pwdCheck.reason);
return;
}
// 校验短信验证码
if (!ctx.session.smsCode || smsCode !== ctx.session.smsCode) {
ctx.body = ctx.helper.error(2005, '短信验证码错误');
@@ -500,6 +518,18 @@ class AuthController extends Controller {
return;
}
// 口令强度校验(R63 · C1)—— 与 activate() 共用同一策略(lib/passwordPolicy.js),
// ⚠️ 禁止在本处另行实现:一旦「忘记密码」通道放宽,弱口令缺口会从另一条路重新打开
// (这正是本模块必须单源的原因)。
// 错误码 7001 = 口令强度不足(resetPassword 段编码;activate 段用 2001)。
// 本入口请求体无 phone 字段(见 validate.resetPassword),故仅传 idNumber 做派生值比对。
const pwdCheck = passwordPolicy.checkPassword(password, { idNumber: id_number });
if (!pwdCheck.ok) {
ctx.logger.info(`[口令策略] 重置口令被拒:rule=${pwdCheck.rule}`);
ctx.body = ctx.helper.error(7001, pwdCheck.reason);
return;
}
if (!ctx.session.smsCode || smsCode !== ctx.session.smsCode) {
ctx.body = ctx.helper.error(7004, '短信验证码错误');
return;