12 KiB
V37 防刷风险概览与运营规则配置
状态:已完成并通过真实 MySQL/Docker 黑盒验收(PASS=76 / FAIL=0 / SKIP=0)。 功能版本:V37;Flyway:
V36__risk_governance.sql。 基线:OpenAPI 148 paths / 182 operations;V35、V36、V37 待建立联合 Git 检查点。
1. 目标与边界
本模块为 ADMIN 提供确定性、可解释的防刷风险概览与规则配置能力。它将既有拒绝型安全审计事件转为可聚合的风险事件,供运营人员查看趋势、分布、明细及进行人工标记。
本版只做以下内容:
- 基于既有
audit_logs的规则型风险检测; - ADMIN 风险概览、趋势、分布、事件列表;
- ADMIN 启停或调整阈值、窗口、等级;
- ADMIN 对风险事件做无自由文本的人工标记;
- 全链路审计、脱敏、幂等扫描与并发保护。
明确不做:
- 自动封禁、自动警告、自动限制投递/消息/评分;
- AI 风控、设备指纹、地理位置、IP 归因、手机号或邮箱展示;
- 修改 V33 举报限流、屏蔽语义,或修改 V35/V36 测评和评分引擎;
- 企业端、学生端风险事件查询;
- 对历史
audit_logs.details的回填、修复或删除。
2. 现有能力与输入范围
现有 SecurityAuditService 已以独立事务写入拒绝型安全审计,当前可安全复用且可关联用户的输入只有:
| 审计 action | 来源 | 初始规则用途 |
|---|---|---|
REPORT_RATE_LIMITED |
V33 举报第 11 次及以后被拒绝 | 一次命中即 HIGH 风险 |
BLOCKED_SEND_PROHIBITED |
V33 屏蔽关系下的新建私聊或发消息被拒绝 | 窗口内多次命中为 MEDIUM 风险 |
SMS_RATE_LIMITED 当前不具备安全审计中的用户主体,且原始手机号不能进入风险库;本版不纳入。后续如需纳入,必须单独设计不可逆主体哈希和密钥治理。
风险计算只读取 audit_logs 的 id/user_id/action/entity_type/entity_id/request_id/created_at,不复制 details/ip_address/user_agent。
3. 风险规则
规则是确定性阈值条件:在 windowMinutes 滑动窗口内,某用户同类安全审计数量达到 thresholdCount 后,生成一条风险事件。事件是提醒,不改变业务权限。
初始种子规则:
| ruleCode | sourceAction | thresholdCount | windowMinutes | severity | 默认启用 |
|---|---|---|---|---|---|
REPORT_RATE_LIMIT_BURST |
REPORT_RATE_LIMITED |
1 | 60 | HIGH | true |
BLOCKED_MESSAGE_BURST |
BLOCKED_SEND_PROHIBITED |
3 | 15 | MEDIUM | true |
规则更新只影响后续扫描;不回溯重算、不会删除既有风险事件。若启用后需重新观察历史,应在未来另行设计“回放”操作,避免误产生大量旧告警。
4. 数据模型与 Flyway
迁移文件为 V36__risk_governance.sql。V36 是功能版本号,Flyway 当前最新迁移为 V35,因此使用严格连续的 V36。
4.1 scoring_rule_configs
| 字段 | 约束/用途 |
|---|---|
id |
PK |
rule_code |
UNIQUE,稳定规则标识 |
source_action |
审计 action 白名单 |
threshold_count |
1–100 |
window_minutes |
1–1440 |
severity |
LOW/MEDIUM/HIGH |
enabled |
是否参与后续扫描 |
version |
@Version 乐观锁 |
created_at/updated_at |
审计时间 |
4.2 scoring_risk_events
| 字段 | 约束/用途 |
|---|---|
id |
PK;对外只返回此事件 ID |
rule_config_id |
FK → scoring_rule_configs |
subject_user_id |
内部关联用户,所有 API 响应均不返回 |
source_audit_log_id |
触发本事件的审计记录;与 rule_config_id 联合唯一,保证扫描幂等 |
source_action |
冗余快照,支持稳定统计 |
severity |
生成时规则等级快照 |
window_started_at/window_ended_at |
命中统计窗口 |
occurrence_count |
命中数量 |
status |
OPEN/ACKNOWLEDGED/DISMISSED |
detected_at |
检测时间 |
version |
乐观锁 |
索引:(detected_at,status)、(severity,detected_at)、(rule_config_id,detected_at)、(subject_user_id,detected_at)。
4.3 scoring_risk_actions
风险事件人工操作历史。为防止运营备注成为新的个人信息载体,本版不提供自由文本 note。
| 字段 | 约束/用途 |
|---|---|
id |
PK |
risk_event_id |
FK → scoring_risk_events |
operator_user_id |
ADMIN 操作人,响应不返回 |
action |
ACKNOWLEDGE/DISMISS |
reason_code |
MONITORING/CONFIRMED_FALSE_POSITIVE/OUT_OF_BAND_ESCALATED |
created_at |
操作时间 |
4.4 scoring_risk_scan_checkpoints
持久化扫描游标,解决应用重启、暂停或扫描耗时导致的审计事件遗漏问题。该表只保留一条 checkpoint_key=RISK_EVENT_DETECTOR 的记录。
| 字段 | 约束/用途 |
|---|---|
id |
PK |
checkpoint_key |
UNIQUE,固定为 RISK_EVENT_DETECTOR |
last_audit_log_id |
已成功处理的最大 audit_logs.id,默认 0 |
version |
乐观锁 |
updated_at |
最近成功推进时间 |
迁移同时为 audit_logs 新增 (action, user_id, created_at, id) 复合索引,仅优化风险扫描,不改变任何既有审计语义。
5. 检测与状态机
5.1 扫描器
RiskEventDetector 使用 @Scheduled 固定延迟扫描(默认 60 秒,可配置 app.risk.scan.fixed-delay-ms),其核心 scanOnce() 必须可被测试直接调用。每轮:
- 在一个事务中以悲观锁或等价机制锁定
RISK_EVENT_DETECTORcheckpoint; - 按
audit_logs.id > last_audit_log_id、升序、每批最多 500 条读取新增审计;仅处理user_id非空且 action 在规则白名单内的记录; - 读取全部
enabled=true的规则。已禁用规则对应的新增审计会随游标跳过;重新启用不回放历史; - 以“当前审计记录时间 - windowMinutes”为窗口,统计同一用户、同一 action 的命中数;
- 仅当命中数恰好等于
thresholdCount时生成事件,避免阈值之后的每次操作都刷出一条风险事件; - 按
(rule_config_id, source_audit_log_id)原子插入事件;唯一键冲突视为已处理,不报错; - 成功处理整批后,在同一事务推进
last_audit_log_id;任何处理失败必须回滚,游标不得前进; - 写入
RISK_EVENT_DETECTED审计,但 details 仅包含riskEventId/ruleCode/severity/occurrenceCount/requestId。
扫描器不处理无用户主体的审计,也不会发送通知、修改用户状态或调用评分引擎。首版允许最长约 60 秒可见延迟。
5.2 事件状态机
OPEN ──ACKNOWLEDGE──> ACKNOWLEDGED
OPEN ──DISMISS──────> DISMISSED
ACKNOWLEDGED ──DISMISS──> DISMISSED
DISMISSED 为终态;不能重新打开。每次合法状态变更新增一条 scoring_risk_actions 记录,并写 RISK_EVENT_ACTIONED 审计。
6. 接口契约(仅 ADMIN)
除 401 外,所有非 ADMIN 情况统一 403/2002。Controller 从 JWT SecurityContext 取身份,禁止接受 adminUserId/operatorUserId/userId 参数。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /admin/scoring/risk/overview |
时间范围内 OPEN 总数、等级分布、规则分布、最近检测时间 |
| GET | /admin/scoring/risk/trend |
按 HOUR/DAY 聚合的事件趋势 |
| GET | /admin/scoring/risk/distribution |
按规则和风险等级聚合的事件分布 |
| GET | /admin/scoring/risk/events |
分页事件列表;可按 status/severity/ruleCode/from/to 筛选 |
| GET | /admin/scoring/risk/rules |
规则列表与当前版本 |
| PUT | /admin/scoring/risk/rules/{ruleCode} |
更新规则,须提供 expectedVersion |
| POST | /admin/scoring/risk/events/{eventId}/actions |
ACKNOWLEDGE/DISMISS 人工操作 |
预期 OpenAPI:141 → 148 paths,175 → 182 operations。
6.1 规则更新请求
{
"enabled": true,
"thresholdCount": 3,
"windowMinutes": 15,
"severity": "MEDIUM",
"expectedVersion": 2
}
不允许修改 ruleCode 或 sourceAction,避免配置被用于监听未批准的审计类型。
6.2 事件操作请求
{
"action": "ACKNOWLEDGE",
"reasonCode": "MONITORING",
"expectedVersion": 0
}
6.3 事件响应白名单
事件列表或详情只含:eventId/ruleCode/sourceAction/severity/status/occurrenceCount/windowStartedAt/windowEndedAt/detectedAt/version。
严禁含:subjectUserId/phone/email/ipAddress/userAgent/requestId/details/Token/password/设备指纹。
7. 错误码
新增 99xx 段:
| code | 名称 | HTTP |
|---|---|---|
| 9901 | RISK_RULE_NOT_FOUND |
404 |
| 9902 | RISK_EVENT_NOT_FOUND |
404 |
| 9903 | RISK_RULE_CONCURRENT_UPDATE |
409 |
| 9904 | RISK_EVENT_CONCURRENT_UPDATE |
409 |
| 9905 | RISK_INVALID_ACTION |
409 |
| 9906 | RISK_INVALID_FILTER |
400 |
复用:未登录 401/2001、无权限 403/2002、分页或时间参数错误 400/1001。
8. 查询、脱敏与性能
- 全部 overview/trend/distribution 使用 SQL
GROUP BY投影,不在 Java 循环中逐事件查询规则或用户,避免 N+1; - 事件列表分页
page=0起、size=1–50;空结果为200 + items=[]; - 默认时间范围为最近 7 天,最大查询范围 90 天;
from > to或超出 90 天返回9906; - 对外 DTO 均使用字段白名单;不返回
subject_user_id; - 不复制
audit_logs.details/ip_address/user_agent至风险事件; - 规则更新和事件操作均乐观锁;冲突不覆盖对方操作;
- 定时扫描使用 checkpoint 断点续扫;事件唯一键保证重复扫描不会重复产生事件,也不会因服务停机而漏掉未处理审计。
9. 审计
| action | 触发点 | details 白名单 |
|---|---|---|
RISK_EVENT_DETECTED |
系统检测到阈值事件 | riskEventId/ruleCode/severity/occurrenceCount |
RISK_RULE_UPDATED |
ADMIN 更新规则 | ruleCode/enabled/thresholdCount/windowMinutes/severity/version |
RISK_EVENT_ACTIONED |
ADMIN 标记风险事件 | riskEventId/action/reasonCode/version |
审计 details 不允许写入用户身份、原始审计 details、IP、浏览器信息、请求正文或任何凭据。
10. 实现与测试计划
新增:4 Entity、4 Repository、RiskEventDetector、RiskGovernanceService、AdminRiskController、请求/响应 DTO、V36__risk_governance.sql、错误码与 OpenAPI 文档。
测试:
- 单元:阈值、滑动窗口、规则禁用、仅阈值首次命中生成事件、checkpoint 断点续扫、扫描失败游标不前进、唯一键幂等、状态机、乐观锁、脱敏;
- MockMvc:ADMIN 成功、STUDENT/COMPANY 403、匿名 401、分页/时间参数、响应不含敏感字段;
- H2 持久化:Flyway、四表 FK/UNIQUE/CHECK、扫描重复执行只生成一条事件、checkpoint 并发锁、规则并发更新;
- MySQL/Docker 黑盒:插入本轮独立的安全审计夹具 → 等待扫描 → 验证概览/趋势/分布/事件/规则/人工操作;验证无自动封禁、无
candidate_dimension_scores写入、队列排空,并回归 V33/V35/V36。
11. 验收与冻结条件
- 专项与全量测试全绿;
verify_openapi.py静态路径/操作数校验通过;- 真实 MySQL Flyway V36 成功,三表/索引/约束可查;
- Docker 黑盒
FAIL=0,关键权限、去重、脱敏、队列、回归项不得 SKIP; - 风险事件不会改变用户状态、投递、消息、评分或候选人维度分;
- 经负责人审阅精确提交范围后,才与 V35/V36 一并建立 Git 检查点。
12. 待负责人确认
建议按本稿锁定以下选择后再编码:
- 规则输入首版仅限
REPORT_RATE_LIMITED与BLOCKED_SEND_PROHIBITED; - 风险事件 API 不返回主体用户 ID,运营通过
eventId处理,不做自动处置; - 保留
POST /admin/scoring/risk/events/{eventId}/actions,因此接口增量为 +7/+7; - 风险事件人工操作仅允许受控
reasonCode,不支持自由文本备注; - 使用 checkpoint 驱动的定时扫描(默认 60 秒)而非把风险检测同步耦合到业务请求;
- 使用
V36__risk_governance.sql与 9901–9906 错误码。