'use strict'; /** * 后端配置共用:**配置密钥解析(唯一入口)** * ============================================================ * 当前覆盖两个同族密钥: * · `JWT_SECRET` —— JWT 签名密钥(`loadJwtSecret`) * · `APP_KEYS` —— Cookie 签名密钥(`loadAppKeys`,Egg `config.keys`) * * 为什么单独成文件(R105-D1 + R106-D6,2026-10-12) * ------------------------------------------------------------ * 原实现(config.default.js 旧 L300/L304 与 L157/L158)各自带**可预测的兜底值**: * const DEFAULT_JWT_SECRET = '<32 位明文串>'; * secret: process.env.JWT_SECRET || DEFAULT_JWT_SECRET * const DEFAULT_APP_KEYS = appInfo.name + '_competition_website_secret_key_2026'; * config.keys = process.env.APP_KEYS || DEFAULT_APP_KEYS * 两者都是「**已入库、任何人可读或可推算**」的值 ⇒ 等价于「密钥公开」: * · JWT_SECRET 公开 ⇒ 任意人可伪造 token 冒充任意身份; * · APP_KEYS 公开 ⇒ 任意人可伪造已签名 Cookie(session / 防篡改校验)。 * 且它们与仓库既有纪律直接冲突: * · `scripts/_creds.py` / `backend/scripts/_db.cjs` 早已统一为「缺凭据明确失败」; * · config.default.js 对 `DB_PASSWORD` 也已改为「缺失即阻断」。 * 同族问题若各自实现必然漂移(R104 的结论)⇒ 收敛到**唯一解析源**,不留兜底。 * * 读取优先级(与 `_db.cjs` / `_creds.py` 同一纪律) * ------------------------------------------------------------ * 1. 对应的**环境变量**(生产由 systemd `EnvironmentFile=` / 启动器注入;本地由 * config.default.js 顶部的 `loadDeployEnv()` 注入 —— 注意 **R106-D9 起 * `JWT_SECRET` / `APP_KEYS` 属"非生产跳过键"**,本地开发不再继承生产值) * 2. 仓库根 `scripts/.creds.local.json` 的对应键(本机开发用;`.gitignore` 已排除; * `jwt_fallback_secret` 同时是 `scripts/_fixtures.py` 的末级来源,两侧同源 ⇒ * 自签 token 与后端验签必然一致) * 3. **明确失败** —— ⛔ **刻意不提供明文兜底**:抛错并给出可行动的修复指引。 * 理由:密钥缺失属"注入链路断了"的真实故障,明确阻断远优于静默用公开值签名。 * * ⚠️ 与 Python 侧 `scripts/_creds.py`(`JWT_FALLBACK_SECRET` / `APP_KEYS`)口径必须一致; * 如需增删键,两边同时改。 */ const fs = require('fs'); const path = require('path'); const LOCAL_FILE = path.join(__dirname, '..', '..', 'scripts', '.creds.local.json'); function _loadLocal() { try { return JSON.parse(fs.readFileSync(LOCAL_FILE, 'utf8')); } catch (e) { if (e.code === 'ENOENT') return {}; // 文件不存在合法(走环境变量) // 损坏必须报错,不静默降级 throw new Error(`凭据文件解析失败 ${LOCAL_FILE}: ${e.message}`); } } /** * 通用解析:env → `.creds.local.json` → 明确失败。 * * @param {object} o * @param {string} o.envKey 环境变量名(如 'JWT_SECRET') * @param {string} o.localKey `.creds.local.json` 中的键名(如 'jwt_fallback_secret') * @param {string} o.purpose 失败时的用途说明(如 '签名/校验登录态') * @param {string} o.builtin 被移除的兜底物名称(如 '内置密钥') * @param {string} o.whyWrong 该兜底值为何不可接受的后果(如 'JWT 可被任意伪造') * @returns {string} 生效中的密钥 */ function _loadSecret(o) { const { envKey, localKey, purpose, builtin, whyWrong } = o; const fromEnv = process.env[envKey]; if (fromEnv) return fromEnv; const local = _loadLocal(); const fromFile = local[localKey]; if (fromFile) return fromFile; throw new Error( `[配置缺失] 未取到 ${envKey} —— 无法安全地${purpose}。请任选其一:\n` + ` ① 设置环境变量 ${envKey}(生产/CI 推荐,deploy/.env.production 已含该键);\n` + ` ② 在 ${LOCAL_FILE} 写入键 "${localKey}"(本机开发用,该文件已被 .gitignore 排除);\n` + `⛔ 本模块刻意不提供明文兜底:原 config.default.js 的${builtin}已移除,\n` + ` 因为「可预测的公开密钥」等价于「${whyWrong}」。` ); } /** * JWT 签名密钥。 * @returns {string} */ function loadJwtSecret() { return _loadSecret({ envKey: 'JWT_SECRET', localKey: 'jwt_fallback_secret', purpose: '签名/校验登录态', builtin: '内置密钥', whyWrong: 'JWT 可被任意伪造', }); } /** * Cookie 签名密钥(Egg `config.keys`)。 * @returns {string} */ function loadAppKeys() { return _loadSecret({ envKey: 'APP_KEYS', localKey: 'app_keys', purpose: '签发/校验 Cookie 签名', builtin: '内置兜底值', whyWrong: 'Cookie 可被任意伪造', }); } module.exports = { loadJwtSecret, loadAppKeys };