Skip to content
bitzorcas
中EN

Reference

RiskControl 测试与生产运维

建立规则边界、Captcha 安全、双 ORM、隐私、故障注入、指标告警和 GA 门禁。

Last updated

风控测试不能只断言“某个分数得到某个 Level”。生产门禁必须证明信号可靠、挑战真的阻断、Ticket 不可盗用、证据不会越租户或过度泄露,并且故障策略符合业务风险等级。

1. 当前已有证据

测试集已覆盖明显缺口
RuleBasedRiskEngineTests租户策略生效、因子异常稳定失败四个真实因子组合、并行与窗口边界
RiskPolicyConfigTests区间完整、无重叠、快照映射管理 Endpoint、并发更新、审批
SettingsRiskPolicyProviderTests读取/配置失败回退、取消传播缓存失效、多实例传播、fallback 告警
RiskAssessmentTestsAOT JSON、深快照、损坏数据拒绝数据库约束、保留、脱敏
RiskAssessmentPersistencePipelineBehaviorTests业务失败/异常后记录、持久化失败隔离真实 Pipeline + 双 ORM、提交不确定性
GetRiskAssessmentsHandlerTests当前租户 Fake Store、过滤分页两个 ORM、Host 模拟、真实授权
LoginRiskIntegrationTests五种 Challenge、Captcha 错答、Engine fail-open真实 Provider / Redis / HTTP
Captcha Provider / Filter未发现专门测试生成、重放、容差、厂商协议、真实挂载

Fake Captcha 可以证明 LoginFlow 检查 Passed,不能证明 Redis GETDEL、Challenge 绑定或 Provider 实现正确。

2. 规则边界表

每个边界都测试下界、上界和相邻值:

分数期望
0、30Low / None
31、50Medium / Captcha
51、75High / MFA
76、90Critical / StepUp
91、100Block
-1、100.01创建拒绝

还要遍历四因子的 16 种命中组合,证明总分、Factor Code、Evidence 与 Challenge 一致,并明确默认 Block 不可达。

3. Captcha 安全合同

一次 Ticket 只能成功一次
[Theory]
[InlineData(CaptchaType.ImageCode)]
[InlineData(CaptchaType.Slider)]
[InlineData(CaptchaType.PointSelect)]
public async Task Ticket_Should_Be_Consumed_Atomically(CaptchaType type)
{
var provider = fixture.Resolve(type);
var challenge = await fixture.GenerateKnownAnswerAsync(provider, "tenant-a", "session-a");
// 第一次正确作答成功,并通过 Redis GETDEL 原子消费。
var first = await provider.VerifyAsync(
new CaptchaVerifyRequest(challenge.Id, challenge.Answer),
CancellationToken.None);
first.IsSuccess.Should().BeTrue();
first.Value!.Passed.Should().BeTrue();
// 相同 Ticket 无论答案是否正确都不能再次通过。
var replay = await provider.VerifyAsync(
new CaptchaVerifyRequest(challenge.Id, challenge.Answer),
CancellationToken.None);
replay.IsSuccess.Should().BeTrue();
replay.Value!.Passed.Should().BeFalse();
}

Provider 使用随机数,测试需要注入可控 Challenge Factory 或解析 RenderData,不能把生产随机逻辑替换为 Fake 后仍称为 Provider 测试。

4. 上下文盗用测试

至少验证:

  • tenant-a / session-a 的 Ticket 不能用于 tenant-b;
  • 同租户 session-a 不能用于 session-b;
  • Login Ticket 不能用于 password-reset / contact;
  • 前缀不匹配时不调用 GETDEL,合法持有者仍能使用;
  • ChallengeId 超长、控制字符、异常分隔段拒绝;
  • 用户名大小写规范与 pseudo-session 计算一致。
错误答案必须被 Endpoint Filter 拒绝
[Fact]
public async Task Filter_Should_Reject_Successful_Result_With_Failed_Verification()
{
provider.VerifyAsync(Arg.Any<CaptchaVerifyRequest>(), Arg.Any<CancellationToken>())
.Returns(Result.Success(new CaptchaVerification(false, "mismatch")));
// Filter 当前会错误调用 next;修复后应返回 400 且业务处理器调用次数为 0。
// 通过真实映射的 Endpoint 发起请求,同时证明 Filter 已正确挂载。
var response = await client.PostAsJsonAsync(
"/api/protected-form",
new { captchaChallengeId = "captcha:tenant-a:session-a:n1", captchaAnswer = "wrong" });
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
protectedHandler.CallCount.Should().Be(0);
}

5. 行为 Provider 合同

用本地 Stub HTTP Server 覆盖:

  1. Aliyun / Tencent 成功、拒绝、损坏 JSON;
  2. 非 2xx、连接失败、DNS、超时和调用方取消;
  3. Provider 非法配置启动失败,而不是静默走 Aliyun;
  4. Secret、Ticket、Randstr 不出现在日志、Trace URL 或 Error Description;
  5. Tencent 使用客户端 Randstr 与真实 UserIp;
  6. 响应体大小、Content-Type 和重定向限制;
  7. SDK URL 只允许 HTTPS 白名单。

外部失败应拒绝当前挑战,但是否自动降级 ImageCode 必须由应用显式发起新 Challenge;不能把已消费的 Behavior Ticket 当作 ImageCode Ticket。

6. 双 ORM 与租户合同

真实数据库插入 tenant-a / tenant-b 同用户名、同 Assessment 时间,再对 EF Core 与 SqlSugar 运行同一合同:

  • 普通 tenant-a 查询只返回 a;
  • Level/User/Time Predicate 与分页下推;
  • 软删除默认不可见;
  • 损坏 FactorsJson 返回稳定 Failure;
  • 正式租户模拟使用 Effective Tenant,而非操作人 CurrentUser Tenant;
  • Host 全局分析走独立权限与审计端口。

7. 故障注入

故障当前预期生产告警
LoginHistory 查询异常Engine Failure,Login 无挑战继续engine_failure + tenant
策略读取/解析异常默认策略policy_fallback + reason
单个 Factor 异常整次 Engine Failurefactor name + latency
Redis 缺失生成成功、所有验证失败startup/readiness red
Redis GETDEL 异常Provider 抛/Failure,请求拒绝ticket_store_error
Captcha 厂商超时Behavior Failure,请求拒绝provider + timeout
RiskAssessment Commit 失败登录结果不变、证据丢失persistence_failure critical
Query 快照损坏API Failureinvalid_snapshot + assessment id

8. 指标与日志

建议低基数指标:

  • risk_assessment_total{level,challenge,source};
  • risk_engine_failure_total{stage};
  • risk_policy_fallback_total{reason};
  • captcha_generated_total{type,provider};
  • captcha_verified_total{type,outcome};
  • captcha_ticket_store_failure_total{operation};
  • risk_assessment_persistence_failure_total;
  • 评估、因子、Provider 的 p50/p95/p99 延迟。

TenantId、UserId、IP、ChallengeId 不放进 Metric Label;它们只在受控结构化日志中以必要、脱敏形式出现。不要记录答案、Ticket 存储值、厂商 Secret 或完整 DeviceInfo。

9. 排障路径

否是否是否是否是

登录异常或攻击未被阻断

Assessment 生成?

检查 Engine Failure / Pipeline / 输入

Challenge 正确?

检查历史、因子、租户策略和时区

消费端执行?

检查 Login 分支 / Filter 挂载 / Passed 判断

Ticket 绑定与 Redis 正常?

检查 prefix、GETDEL、TTL、NoOp Store

检查限流、Provider 协议和自动化绕过

10. 发布前演练

  1. 关闭 Redis,确认 Readiness 阻止安全入口接流量;
  2. 注入策略非法 JSON,确认 fallback 告警和严格租户处置;
  3. 让 Behavior Provider 超时,确认无 Secret 泄露且请求拒绝;
  4. 模拟 RiskAssessment 数据库不可用,确认告警与补偿;
  5. 从 tenant-b 使用 tenant-a Challenge,确认不消费合法 Ticket;
  6. 用错误答案访问挂 Filter Endpoint,确认业务处理器未执行;
  7. 以 Host operate-as 查询目标租户历史,确认权限、数据面和审计一致。

11. GA 门禁

  • Captcha Provider 和 Filter 的真实测试补齐并接入 CI;
  • 所有保护 Endpoint 都有挂载清单与反射/生成式架构测试;
  • Challenge 与 tenant/session/scenario 绑定,错误上下文不消费 Ticket;
  • 行为 Provider Secret 不入 URL,配置和协议符合厂商合同;
  • Engine/Policy/Persistence 的不同降级有指标、告警和租户级策略;
  • 双 ORM、租户模拟、敏感 Evidence 脱敏与保留通过集成测试;
  • 默认四因子可达性与 Block 策略在运营界面清晰展示。

上一篇:集成边界 · 返回 RiskControl

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%