feat(exam/auth): 登录闸门 + 交卷稳定性加固(R4-R8)
R4 登录/交卷背压闸门 - app/middleware/concurrency_guard.js: 新增。超限即 429 + Retry-After(立即拒绝式, 不排队;实测 429 P50=34ms)。RETRY_AFTER 收敛为单一有依据常量 = '2' (≈1.8 个满载窗口;实测 bcryptjs 1129ms / 原生 265ms 每窗口)。 配额:LOGIN_MAX=16 / SUBMIT_MAX=24 / HEAVY_REQ_MAX=20,各 *REQ_MAX 覆盖。 - config/config.default.js: 闸门配额与连接池容量配置;DB dialect 锁 mysql。 R7 顺序红线 + R8 Retry-After 同源 + 原生 bcrypt - 3 处硬编码 '2' 收敛为单一常量;补"勿因换实现而删闸门"红线注释。 - bcryptjs -> bcrypt@6(N-API,Windows/Linux 均免编译;drop-in,存量哈希 $2b$10$ 兼容)。 - app/model/account.js / app/service/auth.js: 同步哈希实现注释(D4 全族 5 处陈旧 SHA256 注释修正)。 答题链路与综合大题 - app/controller/exam.js: 中职/高职综合大题接口(GET big-question / POST big-answer, 逐问 2000 字上限校验);uploadWord 对中职/高职封堵(400)。 - app/service/exam.js: 抽题快照 question_snapshot 落库、答案归集键修正。 - app/controller/auth.js / app/service/auth.js: toClientUser() 唯一出口, 未发布态按组剔除 score,exam_score/word_score/ppt_score 任何情况下不下发。 - app/router.js: 新增大题与管理端路由。 - app/extend/helper.js: 下载文件名 RFC5987 编码(禁双重编码)。 - app.js: 池容量断言按 env 区分单/多 worker(防 dev 假日志)。 验证:ci-gates.sh 9/9 全绿;verify_login_guard.py 28/28(含 D 组变异 9/9); 真实登录/抽题/交卷端到端 code=0;前端构建 BUILD_RC=0。
This commit is contained in:
@@ -26,6 +26,21 @@ module.exports = app => {
|
||||
// 登录限流中间件(防暴力破解):15 分钟内同一 IP+账号最多 10 次失败
|
||||
const loginRateLimit = middleware.loginRateLimit({ windowMs: 15 * 60 * 1000, maxAttempts: 10 });
|
||||
|
||||
// 交卷限流(2026-09-21 并发压测结论)
|
||||
//
|
||||
// 🔴 只挂在 submit-all 这**单条**路由上,绝不整段挂 /api/exam:
|
||||
// 逐题保存 submit-answers 是「每答一题一次请求」(一场约 59 次),若整段挂
|
||||
// exam:30/分钟,正常考生在答题过程中就会被自己的保存请求打爆(429),
|
||||
// 直接造成丢答案 —— 这比不限流更糟。
|
||||
//
|
||||
// 🔴 必须排在 auth **之后**:rateLimit 的限流键优先取 ctx.state.currentUser.uid
|
||||
// (见 middleware/rateLimit.js getClientId),而 currentUser 由 auth 写入。
|
||||
// 若不排在 auth 之后,键会退化为 ip:<ctx.ip> —— 考场/学校普遍共用出口 IP,
|
||||
// 会把整间考场的考生一起限流拒掉,属灾难级误伤。
|
||||
//
|
||||
// 阈值取 LIMITS.exam = 30 次/60 秒(正常交卷 1 次 + 前端自动重试数次,余量充足)。
|
||||
const examSubmitLimit = middleware.rateLimit({ level: 'exam' });
|
||||
|
||||
// ==================== 公开接口(无需认证) ====================
|
||||
|
||||
// 健康检查(容器探针 / 负载均衡探活,无需认证)
|
||||
@@ -72,7 +87,9 @@ module.exports = app => {
|
||||
router.post('/api/exam/:group/submit-answers', auth, controller.exam.submitAnswers);
|
||||
router.post('/api/exam/:group/upload-word', auth, controller.exam.uploadWord);
|
||||
router.delete('/api/exam/:group/word', auth, controller.exam.deleteWord);
|
||||
router.post('/api/exam/:group/submit-all', auth, controller.exam.submitAll);
|
||||
// 交卷:全局闸门(config.middleware 的 concurrencyGuard 已纳入 submit-all,
|
||||
// 过载返回 429 + Retry-After)→ auth → 交卷限流(按 user,见上方 examSubmitLimit)
|
||||
router.post('/api/exam/:group/submit-all', auth, examSubmitLimit, controller.exam.submitAll);
|
||||
|
||||
// 综合大题(仅中职/高职组;2026-09-21 起替代原 Word 文件上传题)
|
||||
// 首次 GET 抽题并落库题干快照,后续 GET 一律返回同一道题(幂等防纠纷)。
|
||||
|
||||
Reference in New Issue
Block a user