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

61
scripts/_db.cjs Normal file
View File

@@ -0,0 +1,61 @@
'use strict';
/**
* 后端运维脚本共用:数据库连接配置解析(**唯一入口**)
* ============================================================
*
* 为什么单独成文件
* ------------------------------------------------------------
* 任何"需要连库"的运维脚本(弱口令扫描 / 口令策略接口验证 / 后续新增)都必须
* 用同一套优先级与同一条凭据纪律。各自实现必然漂移 —— 例如某处"缺口令时兜底成
* 空串"就能静默连上无口令的库,而另一处明确失败,两者行为不一致却都"看起来正常"。
*
* 读取优先级(与 scripts/_creds.py 完全同源)
* ------------------------------------------------------------
* 1. 环境变量 DB_HOST / DB_PORT / DB_USER / DB_PASSWORD / DB_NAME(CI / 服务器注入优先)
* 2. 仓库根 `scripts/.creds.local.json`(本机开发,已被 .gitignore 排除)
* 3. 明确失败 —— ⛔ **刻意不提供明文兜底**:口令为空时直接抛错,
* 绝不静默尝试空口令连库(那会掩盖"凭据没注入"这一真实故障)。
*
* ⚠️ Python 侧对应实现见 `scripts/_creds.py`,两侧口径必须一致;
* 如需增删键,**两边同时改**。
*
* @returns {{host: string, port: number, user: string, password: string, database: string}}
*/
const fs = require('fs');
const path = require('path');
function loadDbConfig() {
const env = process.env;
let local = {};
// backend/scripts/ → 仓库根/scripts/.creds.local.json
const localFile = path.join(__dirname, '..', '..', 'scripts', '.creds.local.json');
try {
local = JSON.parse(fs.readFileSync(localFile, 'utf8'));
} catch (e) {
// 文件不存在是允许的(走环境变量);但**损坏**必须报错,不静默降级
if (e.code !== 'ENOENT') {
throw new Error(`凭据文件解析失败 ${localFile}: ${e.message}`);
}
}
const cfg = {
host: env.DB_HOST || local.db_host || '127.0.0.1',
port: parseInt(env.DB_PORT || local.db_port || 3306, 10),
user: env.DB_USER || local.db_user || 'root',
password: env.DB_PASSWORD || local.db_password || '',
database: env.DB_NAME || local.db_name || 'competition_db',
};
if (!cfg.password) {
throw new Error(
'未取到数据库口令。请任选其一:\n' +
' ① 设置环境变量 DB_PASSWORD\n' +
' ② 创建 scripts/.creds.local.json(形如 {"db_password":"..."})\n' +
'⛔ 本模块刻意不提供明文兜底:缺凭据时明确失败,不静默连库。'
);
}
return cfg;
}
module.exports = { loadDbConfig };