Skip to content
bitzorcas
中EN

Guide

RiskControl Identity、Website 与查询集成

逐段解释 LoginFlow、风险驱动 MFA、Website 联系表单、评估查询、权限与租户模拟边界。

Last updated

RiskControl 的价值在消费端体现。同一个 Challenge 在 Login、MFA 策略和匿名 Website 表单里有不同的失败策略、主体信息和业务时序,不能用一段通用伪代码概括。

1. LoginFlow:密码前评估

Login.Command 实现 IRiskAssessedRequest,因此既会调用引擎,也会在外层 Pipeline 持久化成功评估。执行顺序:

  1. 用请求 TenantId、UserName、IP、UserAgent、DeviceId 构造风险上下文,UserId 为空;
  2. 引擎失败记录 Warning,并按 None 继续;
  3. Block 在用户查询和 BCrypt 前拒绝并写失败登录日志;
  4. Captcha 首次生成挑战并返回无 Token 响应,二次提交验证;
  5. MFA / StepUp 设置 riskMfaRequired,继续验证密码;
  6. 密码成功后,用户 2FA 与风险 MFA 取并集,再签发 MFA Challenge;
  7. 只有所有步骤通过才签发访问、刷新 Token 和 UserSession。
NoneCaptcha / no ticketchallenge responseCaptcha / ticketPassedFailedBlockMFA or StepUpinvalid credentials/statusrisk or account requiresMFAno MFAMFA response

Assess

Password

CaptchaIssue

CaptchaVerify

Rejected

Mfa

Token

2. Login 的 Captcha 合同

LoginResponse 用 RequiresCaptcha、ChallengeId、RenderData 表示第一阶段,不签发 AccessToken/RefreshToken。第二次请求必须带 Id 和 Answer。

客户端处理两阶段登录
var first = await auth.LoginAsync(credentials, cancellationToken);
if (first.RequiresCaptcha)
{
// RenderData 的解释必须依据服务端返回的 Captcha 类型协议。
var answer = await captchaUi.SolveAsync(
first.CaptchaChallengeId,
first.CaptchaRenderData,
cancellationToken);
// 二次提交复用相同 tenant、username 与凭据,并带回一次性 Ticket。
// 第一阶段不得缓存或伪造 Token;只有第二阶段完整通过才接受登录结果。
var second = credentials with
{
CaptchaChallengeId = first.CaptchaChallengeId,
CaptchaAnswer = answer,
};
return await auth.LoginAsync(second, cancellationToken);
}
return first;

当前 LoginResponse Projection 未暴露 CaptchaType,只返回 RenderData;Login 又固定 ResolveDefault,因此目前可按 ImageCode 处理。将来选择 Slider/Behavior 时必须版本化响应协议。

3. RiskDrivenMfaPolicyService 是另一条评估路径

Identity Infrastructure 的 RiskDrivenMfaPolicyService 将 MfaRiskContext 再映射为 LoginRiskContext:

LevelMFA Requirement
LowTOTP,可由受信设备跳过
MediumTOTP,不允许受信设备跳过
High / CriticalTOTP + FIDO2
BlockDeny
Engine 缺失、异常或 FailureTOTP,不允许受信设备跳过

这里所谓 fail-open 仍要求 TOTP,和 LoginFlow 的“无挑战继续密码”不同。文档与告警必须区分调用点。

同一请求若先由 LoginFlow 评估、后又由 MFA Policy 评估,会尝试向 RiskAssessmentExecutionContext Capture 两次,第二次抛 InvalidOperationException 并被引擎包装为 Failure。接入时应复用第一次 Assessment,而不是重复评估。

4. Website 联系表单

GET /contact/captcha 使用服务端 AnonymousVisitorContext.VisitorId 生成:

captcha:public-website:{visitorId}:{nonce}

提交联系表单时,Handler 先验证 ChallengeId 前缀,再调用默认 Provider,并同时检查 Failure 和 Passed。验证码通过后才校验和保存 PII 线索。

这个用例比通用 Endpoint Filter 更完整,但还应叠加 Visitor/IP 限流、挑战生成频率限制和 Contact 去重。Captcha 本身不能阻止攻击者无限生成挑战。

5. 通用 Endpoint Filter 的当前状态

RequireCaptchaAttribute 和 RequireCaptchaEndpointFilter 是 Host 脚手架。源码备注明确“尚未挂载到任何 Endpoint”。Filter 读取 JSON Body 两个字段并使用默认 Provider。

当前判断:

当前 Filter 漏检业务失败
var verifyResult = await resolver.ResolveDefault().VerifyAsync(
new CaptchaVerifyRequest(challengeId, answer),
httpContext.RequestAborted);
// 当前只拒绝基础设施 Result Failure。
if (verifyResult.IsFailure)
return Results.BadRequest();
// Provider 对错误答案返回 Success(Passed=false),这里仍会进入业务 Endpoint。
return await next(context);

修复后还要验证 Challenge 前缀、Body 大小、Content-Type、错误映射,并对真实 Endpoint 做 HTTP 集成测试。仅在类上放 Attribute 不会自动让 Source-Generated Endpoint 挂 Filter。

6. 风险评估查询 API

GetRiskAssessments.Query 生成 GET /api/risk/assessments,声明:

  • Resource:module risk-control,resource assessment;
  • Action:View;
  • Permission Catalog:risk-control.assessment.view;
  • 可选 UserId、Level、From、To 与分页;
  • 非法 Level / 倒置时间返回 Validation;
  • 非法页码回退 1 / 20,合法上限 100。

Handler 用 ICurrentUser.User.TenantId 建租户基线,Store 再显式 Tenant Predicate。普通用户正确,但正式租户模拟需要审查:

评估查询应使用 Effective Tenant
public static class TenantErrors
{
public static readonly Error Required =
Error.Forbidden("Tenant.Required", "A trusted tenant is required.");
}
var effectiveTenantId = currentTenant.Tenant.EffectiveTenantId;
if (!TenancyDefaults.IsValid(effectiveTenantId))
{
// 缺少可信有效租户时直接拒绝,不能回退到请求参数。
return Result.Failure<RiskAssessmentPage>(TenantErrors.Required);
}
// Tenant 不接受 Query 参数,来自多租户运行时的有效数据面。
var page = await store.SearchAsync(
effectiveTenantId,
filter,
pageRequest,
cancellationToken);

是否允许 Host 查看目标租户风险历史还需要独立权限、操作原因和审计,不能只靠 CurrentTenant 切换。

7. Factors DTO 的隐私边界

RiskAssessmentItem 直接包含 IReadOnlyList<RiskFactor>。当前 Evidence 可能暴露:

  • NewIp.ip 原始 IP;
  • NewDevice.deviceFingerprint 原始设备信息或 UserAgent;
  • OffHours.businessNow 完整本地时间;
  • 失败次数和历史集合大小。

建议拆成:

  1. 列表 DTO:Id、UserId 别名、Level、Score、Challenge、Factor Codes、AssessedAt;
  2. 敏感详情 DTO:经更高权限和审计返回脱敏 Evidence;
  3. 导出:独立审批、Purpose 和短时下载。

8. 跨模块依赖方向

RiskControl.Contracts 提供窄端口;Identity Infrastructure 实现 LoginHistory Provider,Identity Application 消费 Engine/Captcha。RiskControl Infrastructure 不引用 EF Core 或 SqlSugar,依赖 IEntitySet 和 QueryShape。

RiskControl Contracts

RiskControl Application

Identity Infrastructure
LoginHistoryProvider

Identity Application
LoginFlow

Website Application

API Host
Captcha Filter

RiskControl Infrastructure
Store

依赖方向看似跨模块,实际通过 Contracts 窄端口隔离:RiskControl 不读取 Identity Aggregate,Identity 不依赖 RiskControl Infrastructure。

9. 集成测试清单

  • Login 的五种 Challenge 分支与密码/MFA/Token 时序;
  • Engine Failure 在 Login 与 MFA Policy 的不同降级;
  • Captcha 第一阶段不泄露用户存在性,不签发 Token;
  • Login Ticket 跨 tenant/username 盗用拒绝;
  • Website Ticket 跨 Visitor 拒绝并保持合法 Ticket 可用;
  • Filter 实际挂载、错误答案拒绝、Body 可重复读取;
  • 查询 API 权限、Effective Tenant、分页、损坏快照和 Evidence 脱敏;
  • 同一请求多次风险评估不覆盖或丢失事实。

上一篇:策略与持久化 · 下一篇:测试与生产运维

100%

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