Skip to content
bitzorcas
中EN

Tutorial

再认证快速接入

把一个业务端点接入敏感操作再认证的三条路径:管理页零代码自建策略行、[RequireStepUp] 声明基线、资金类一次性票据;含每步验证点与预期失败修复。

Last updated

本教程把一个真实业务端点——「移除团队成员」——从零接入敏感操作再认证。三条路径对应三种接入姿态,按业务敏感度选择:批量运营操作走策略行(零代码)、平台硬约束走声明基线(一行特性)、资金类操作用一次性票据(当前为规划能力,页内给出替代方案)。三条路径可叠加:同一端点可以先声明基线兜底,再由租户策略行按人群收窄。

前置条件:

  • BitzOrcas API Host 可运行,Redis 已连接(凭据与撤销版本存储依赖 Redis);
  • 演示账号具有 identity.stepup.manage 权限(种子默认授予 root-admin / tenant-admin / host-admin);
  • 功能总闸 identity.stepup 已开启(Host 管理端「功能分发管理」页,种子默认关闭——关闭时全链路零行为,这是设计上的安全回滚通道)。

最终产物:未携带凭据调用该端点得到结构化 403;完成一次验证后携带凭据重放成功;窗口期内重复调用不再弹验证。

运营批量操作平台硬约束资金类一次性

接入决策

路径一:策略行

路径二:声明基线

路径三:规划中

验证 403 引导

完成验证并重放

准备:确认总闸与目录

先确认 Feature 总闸与用途目录,避免”配置了却不生效”的困惑。

Terminal window
# 在仓库根目录启动 API Host(环境按部署文档装配,需含 Redis 连接)
dotnet run --project src/Hosts/BitzOrcas.Api
# 用管理员令牌查询当前用户会被要求二次验证的用途清单;
# 返回空数组通常意味着总闸关闭或尚无任何策略行/声明基线。
curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
-X QUERY https://localhost:5001/api/identity/step-up/my-purposes

验证点:Host 启动日志无 Step-Up 相关异常;my-purposes 返回 200。若 Host 使用 Testing 环境或未配置 Redis,identity.stepup 会被锁死为关闭——这是无 Redis 部署的防误开启设计,不是故障。

路径一:管理页自建策略行(零代码)

适合”某租户的某类操作需要加验”的运营诉求,全程不改编译产物。

  1. 以租户管理员登录,进入「组织与访问控制」→「再认证策略」Tab;
  2. 点击「新增保护」,目录选择器里搜索目标权限码(例如 identity.user.remove)——目录只读、无自由文本,已有行的用途会被置灰并说明原因;
  3. 强制级别先选「仅观察」(AuditOnly,表单常驻此建议提示),窗口 300 秒,允许因子保持 totp + emailOtp,范围「全员」;
  4. 保存。列表出现新行,行属标签为「租户覆盖」,「最近修改」列记录操作人与时间;
  5. 切到「强制」前,先在观察期收集 StepUpAuditOnlyObserved 审计(安全审计页按 Module=Identity、动作过滤),确认命中量与用户体验可接受后再切换。

等效的管理面 API 调用(供自动化流水线使用):

Terminal window
# 创建策略行:范围 allUsers、窗口 300 秒、仅观察;
# expectedVersion 不需要(新建);冲突场景(同用途同范围已有行)返回 422 PolicyConflict。
curl -s -X POST https://localhost:5001/api/identity/step-up/policies \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"purposeCode": "identity.user.remove",
"enforcement": "auditOnly",
"windowSeconds": 300,
"factors": ["totp", "emailOtp"],
"scopeType": "allUsers",
"scopeValues": [],
"unenrolledBehavior": "fallbackFactors"
}'

验证点:返回 200 与行投影(含 id、version);再次打开管理页能看到该行;随后无凭据调用一次成员移除端点——观察模式下请求放行,但安全审计页出现 StepUpAuditOnlyObserved 记录。

预期失败一(护栏拒绝):把窗口改成 1200 秒再保存,得到 422 Identity.StepUp.PolicyConflict,detail 为「窗口秒数超过 Host 上限 900。」——租户行受 Host 控制面窗口上限约束,这是逐字段拒绝而非整体失败。修复:改回上限内的窗口;确需更长窗口时由平台管理员在 Host「安全基线」页调高上限(上限受 86400 秒技术防呆天花板约束)后再保存。

预期失败二(重复行):同一用途、同范围再建一行,得到 422 PolicyConflict「同租户、同用途、同作用范围已存在策略行。」。修复:编辑既有行而不是新建。

路径二:声明基线(一行特性)

适合”这个操作在任何租户都必须再认证”的平台硬约束。在业务端点上挂 RequireStepUpAttribute——它是纯元数据,强制逻辑仍在全局中间件:

src/Modules/Business/YourModule/Endpoints/TeamMemberEndpoints.cs
// 最小 API 端点挂声明基线:用途码默认即权限码,无需重复声明。
group.MapDelete("/api/business/teams/{teamId}/members/{userId}", ...)
.RequirePermission("identity.user.remove") // RBAC 仍然先行:无权限者到不了再认证
.WithMetadata(new RequireStepUpAttribute("identity.user.remove")
{
// 以下均为"无策略行时的基线默认值",租户策略行可在护栏内覆盖。
Mandatory = false, // 平台强制用途置 true:租户不可关闭或放宽
DefaultEnforcement = StepUpEnforcement.Required, // 基线强制级别(默认即 Required)
DefaultWindowSeconds = 300, // 基线窗口;0 表示一次性票据
DefaultFactors = [StepUpFactor.Totp, StepUpFactor.EmailOtp],
});
Terminal window
# 编译并启动,确认声明被冻结目录收录:
dotnet build src/Hosts/BitzOrcas.Api
# 验证点一:无凭据调用得到引导型 403,响应根平铺 purpose/factors/windowSeconds。
curl -s -i -X DELETE https://localhost:5001/api/business/teams/t1/members/u9 \
-H "Authorization: Bearer $USER_TOKEN" | grep -E "HTTP|type|purpose|errorCode"

预期输出包含 HTTP/1.1 403、type: .../problems/step-up-required、purpose: identity.user.remove、errorCode: Identity.StepUp.GrantRequired。

声明集在组合根启动时冻结:同一用途码多处声明但基线参数不一致会启动失败(快速失败,防止”哪个基线生效”变成玄学)。对于源生成器端点无法携带端点元数据的场景,平台在组合根提供代码基线声明清单(BitzOrcasVNext 仓库 src/Hosts/BitzOrcas.Api/StepUp/StepUpBaselineDeclarations.cs,试点用途为 operations.sql-masking.manage:平台强制 + 一次性票据),清单中的每个用途都要通过接线校验——必须存在以同权限码承载的真实端点,否则宿主拒绝启动,杜绝”声明悬空后静默失去保护”。同一端点没有特性时,中间件自动落到端点权限码用途——所以”权限码即用途”的端点甚至可以零声明,由纯策略行驱动。

完成一次验证闭环

三条路径共用同一个验证闭环。以邮箱因子为例(用户未绑定 TOTP 时的降级通道):

POST /api/identity/step-up/challenge HTTP/1.1
Authorization: Bearer <access-token>
Content-Type: application/json
{ "purpose": "identity.user.remove" }
{
"challengeId": "9fJ2vQ8rT3wL5nX1cZ7bA0dK4mH6pS2yE8uG1iO5aB3",
"expiresInSeconds": 120,
"windowSeconds": 300,
"factors": [
{ "type": "emailOtp", "target": "l***@example.com" }
]
}
POST /api/identity/step-up/otp/send HTTP/1.1
Authorization: Bearer <access-token>
Content-Type: application/json
{ "challengeId": "9fJ2vQ…", "factorType": "emailOtp" }

验证码到手后提交验证,响应中的 stepUpToken 就是再认证凭据:

POST /api/identity/step-up/verify HTTP/1.1
Authorization: Bearer <access-token>
Content-Type: application/json
{ "challengeId": "9fJ2vQ…", "factorType": "emailOtp", "code": "482913" }
{
"stepUpToken": "Ak7Qx2Wm9Rf4Tt1Lz8Nb0Vc5Yh3Jd6Pg2Se9Ua1Wr4X=",
"purpose": "identity.user.remove",
"windowSeconds": 300,
"expiresAt": "2026-09-02T08:15:00Z"
}
Terminal window
# 重放业务请求:凭据经专用响应头提交,头名可配(出厂 X-Step-Up-Token)。
curl -s -X DELETE https://localhost:5001/api/business/teams/t1/members/u9 \
-H "Authorization: Bearer $USER_TOKEN" \
-H "X-Step-Up-Token: Ak7Qx2Wm9Rf4Tt1Lz8Nb0Vc5Yh3Jd6Pg2Se9Ua1Wr4X="

验证点:

  • 窗口期内(300 秒)再次调用同一用途端点,无需再带新验证——窗口凭据可重复使用;
  • 第一方 SPA 中以上全部由 SDK 拦截器自动完成,业务代码无感(见前端接入);
  • 登出后立刻用旧凭据重放 → 403 GrantInvalid(登出撤销版本递增)。

预期失败三(频控):一分钟内对同一挑战重发 OTP 超过间隔(出厂 60 秒),otp/send 返回 429 Identity.StepUp.RateLimited。修复:按响应 retryAfterSeconds 倒计时后重试;前端弹窗已内置该倒计时。

资金类操作:一次性票据(规划中)

资金类操作的理想形态是”验证一次、执行一次”:凭据为一次性票据(windowSeconds: 0),业务命令内联消费,杜绝校验通过后凭据被复用的检查-执行间隙(TOCTOU)。该命令内联模式当前为规划能力,尚未交付——不要在手册之外的实施里假设它已存在。

当前替代方案(已交付语义):为资金类端点声明 DefaultWindowSeconds = 0 的基线或建一次性策略行,让凭据消费即失效;业务 Handler 内保留金额、状态机与幂等键校验,把”验证通过但业务失败”的票据损失收敛为一次重新验证。命令内联的架构仲裁记录见 BitzOrcasVNext 仓库 ADR 0706;窗口选择权衡的完整讨论见概念页安全语义。

收尾:把接入固化下来

  • 把端点的声明基线、单元测试(断言无凭据 403 契约)一起提交;测试写法可参考 BitzOrcasVNext 仓库 tests/BitzOrcas.Integration.Tests/Identity/StepUpEnforcementEndpointTests.cs;
  • 灰度顺序(先 AuditOnly 后 Required)与故障排查见灰度与运维;
  • 契约细节、错误码全表与配置键见契约参考。

100%

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