Skip to content
bitzorcas
中EN

Reference

Identity 配置、权限与外部集成

Identity 的密码、客户端加密 Cipher、JWT、密钥轮换、前端链接、MFA、外部身份、通知、权限目录和生产适配器参考。

Last updated

本页回答三个生产问题:配置键由谁读取、缺省时会发生什么、怎样确认真实适配器已经覆盖占位实现。配置文件能让 Host 启动,不代表身份能力已经可以商业交付。

1. 注册顺序

组合根先注册 Identity 应用服务,再注册 Infrastructure 适配器。TryAdd 默认实现允许后续真实适配器覆盖;顺序错了可能让 Null 或 Unavailable 实现继续生效。

Host 中的推荐装配顺序
// ① CoreRuntime 内部注册 Identity 应用策略、增强 MFA 条件入口和失败关闭默认端口。
services.AddBitzOrcasCoreRuntime(configuration);
// ② 同时具备数据库与 RabbitMQ 时,注册生成式 ORM Store、通知与身份联邦适配器。
// 配置不完整时该方法返回,API Shell 继续使用 CoreRuntime 的默认端口。
services.AddBitzOrcasPersistenceAdapters(configuration);
// ③ 后续注册认证 Scheme 和 API Pipeline;它们消费上面已闭合的 Identity 服务图。
services.AddBitzOrcasAuthentication(configuration, environment);
services.AddBitzOrcasApiPipeline(configuration);

AddBitzOrcasCoreRuntime 内部调用 AddBitzOrcasPlatformIdentity 与 AddBitzOrcasMfaConnectors;AddBitzOrcasPersistenceAdapters 在生产分支调用通知连接器和 AddBitzOrcasIdentityFederationConnectors。迁移 Host 时应从现有组合根复制已验证的调用关系,而不是重复调用内部扩展。

2. 密码配置

配置键默认值作用
Identity:Password:RequireDigittrue至少一个数字
RequireLowercasetrue至少一个小写字符
RequireUppercasetrue至少一个大写字符
RequireNonAlphanumerictrue至少一个符号
RequiredLength8最小字符数
RequiredUniqueChars1最少不同字符数
Identity:Password:BCrypt:WorkFactor12BCrypt 成本
Identity:Password:Expiry:PasswordExpiryDays900 表示永不过期
PasswordExpiryWarningDays7提前提醒
Identity:Password:History:PasswordHistoryCount5历史策略配置值

2.1 密码客户端加密 Identity:Password:Cipher

绑定类型:PasswordCipherOptions(节名常量 PasswordCipherOptions.SectionName)。

产品底座 强制 RSA-OAEP-SHA256 客户端加密;无明文 password 回退。未写本节时使用下表默认值即可登录(SDK 会拉 GET /api/auth/cipher-key 并加密)。

配置键默认值合法 / 生效范围越界行为作用
Identity:Password:Cipher:KeySize20482048–4096ValidateOnStart 失败,进程起不来;运行时创建密钥时若仍越界会回落 2048RSA 密钥位数;分布式模式保护 PKCS#8 私钥快照,内存降级模式持有进程内 RSA
RotationHours241–720(上限 720 小时 = 30 天)不做启动校验;CipherKeyRotationHostedService 用 Math.Clamp(..., 1, 24*30) 静默钳制后台密钥轮换周期(小时)。写 0 会被当成 1;写 1000 会被当成 720
GracePeriodMinutes10运行时下限 1 分钟;无启动上限校验Math.Max(1, GracePeriodMinutes);过小/负数变 1,过大不会被拒绝旧密钥仍可解密的宽限窗口(分钟)。应 远小于 轮换周期;推荐 5–30。客户端遇 KeyExpired 时 SDK 会刷新公钥重试一次
MaxClockSkewSeconds12030–600(10 分钟)ValidateOnStart 失败载荷时间戳相对服务端时钟允许偏差;同时经 cipher-key 响应下发给客户端做时钟校准
NonceTtlSeconds12030–600(10 分钟)ValidateOnStart 失败;领取 Nonce 时再 Clamp(30, 600)Nonce 一次性缓存 TTL(秒);防重放
appsettings.Production.json 中的密码策略 + 客户端加密
{
"Identity": {
"Password": {
"RequireDigit": true,
"RequireLowercase": true,
"RequireUppercase": true,
"RequireNonAlphanumeric": true,
"RequiredLength": 12,
"RequiredUniqueChars": 6,
"BCrypt": {
"WorkFactor": 12
},
"Expiry": {
"PasswordExpiryDays": 90,
"PasswordExpiryWarningDays": 7
},
"History": {
"PasswordHistoryCount": 5
},
"Cipher": {
"KeySize": 2048,
"RotationHours": 24,
"GracePeriodMinutes": 10,
"MaxClockSkewSeconds": 120,
"NonceTtlSeconds": 120
}
}
}
}

校验与生效语义(务必分清)

字段启动 ValidateOnStart运行时
KeySize是(2048–4096)创建密钥时再兜底到 2048
RotationHours否Clamp(1, 720);超出 720 不会阻止启动,只会按 30 天轮换
GracePeriodMinutes否至少 1 分钟
MaxClockSkewSeconds / NonceTtlSeconds是(30–600)Nonce TTL 领取时再次 clamp

因此:开发同学若把 RotationHours 配成 8760(一年)以为更省事,进程能起来,但实际仍按 720h(30 天)轮换——必须以本文档或源码 Math.Clamp(..., 1, 24 * 30) 为准,而不是“配置写了什么就是什么”。

密钥生命周期与 cipher-key 响应

项行为(与源码一致)
生成时机分布式模式在缓存键首次缺失时生成共享首对密钥;GetOrCreateAsync 抑制多实例同时回源。内存降级模式才在每个 API 进程启动时生成
keyId{yyyyMMddHHmmss}-{6 位 hex},例如 20260811120000-a1b2c3
公钥格式RSA SubjectPublicKeyInfo(SPKI) Base64;算法标识固定 RSA-OAEP-SHA256
私钥分布式模式导出 PKCS#8 后立即经独立 Data Protection purpose 保护并清零明文字节,快照存缓存;内存模式仅持有进程内 RSA。两种模式都不写配置、不记日志
轮换节奏CipherKeyRotationHostedService 先 sleep RotationHours,再 RotateNow();不是启动立刻轮换。轮换时当前钥降为 previous,宽限 GracePeriodMinutes 后 purge
环容量同时最多 当前 + 上一把 两把可解密;更早的 keyId 一律 KeyExpired
分布式缓存键identity:cipher:keyring:v1;TTL 覆盖轮换周期、宽限期和 1 小时缓冲,并带 5% jitter
GET /api/auth/cipher-key返回 keyId、publicKey、algorithm、serverTimestamp(UTC 毫秒)、maxClockSkewSeconds
载荷明文拼接 nonce|timestamp|password 再 RSA-OAEP-SHA256;password 可含 |(只按前两个分隔符切分)
Nonce 缓存键区域 identity:cipher:nonce + nonce 值;优先 ICacheStore

运维注意与当前边界

  • 水平扩展 / 多实例:同时具备 ICacheStore 与 ICipherKeyMaterialProtector 时,current + previous 快照在分布式缓存共享,实例 A 下发的公钥可由实例 B 解密。所有实例必须共享同一 ASP.NET Core Data Protection key ring;否则私钥解保护失败并返回 KeyExpired。
  • 降级模式:缓存或保护器任一缺失都会记录 Warning,并退回进程内密钥环。此时跨实例请求仍可能 KeyExpired;生产应修复接线,短期才考虑 sticky。平台 SDK 的一次刷新重试只是竞态缓冲,不是多实例方案。
  • Nonce 存储优先 ICacheStore(如 Redis);未配置时进程内 ConcurrentDictionary 降级并打 Warning——多实例下重放防护只在单节点有效,生产应接 Redis。
  • 算法固定 RSA-OAEP + SHA-256;载荷格式与流程见 登录安全。
  • 调大 KeySize(3072/4096)会增加登录 CPU 与密文长度,上线前压测。
  • RotationHours 过大(贴近 720)会拉长单钥暴露窗口;过小会增加 KeyExpired 与 SDK 重试频率。默认 24 适合多数部署。
  • GracePeriodMinutes 应覆盖「客户端已缓存公钥 → 提交」的最大合理延迟,并小于轮换周期;不要配成数小时级宽限稀释轮换收益。
  • 相关错误码:Identity.Cipher.KeyExpired、DecryptFailed、ClockSkewExceeded、ReplayDetected、EncryptedPasswordRequired、PayloadInvalid。

提高 BCrypt WorkFactor 前要以生产规格压测登录吞吐和 CPU。不能为了吞吐把 WorkFactor 降到安全基线以下,也不能在没有容量验证时直接翻倍导致认证雪崩。

3. JWT 基础配置

键默认值生产要求
Jwt:IssuerBitzOrcas与验证端一致
Jwt:AudienceBitzOrcas与 API 资源一致
Jwt:Secret空至少 32 字符,只能来自秘密管理系统
Jwt:AccessTokenExpirationMinutes15缩短会增加刷新压力,延长会扩大泄露窗口
Jwt:RefreshTokenExpirationDays7与 Session 默认 7 天语义对齐

开发环境可使用单一 Secret;生产优先配置带 Kid 的 SigningKeys,让签发选择当前密钥、验证接受宽限期内的旧密钥。

JWT 密钥环与轮换配置
{
"Jwt": {
"Issuer": "https://identity.example.com",
"Audience": "bitzorcas-api",
"AccessTokenExpirationMinutes": 15,
"RefreshTokenExpirationDays": 7,
"SigningKeys": [
{
"Kid": "identity-2026-q3",
"Secret": "secret-injected-by-managed-key-provider-at-runtime",
"ActiveFrom": "2026-07-01T00:00:00Z",
"ActiveTo": null
}
],
"Rotation": {
"Enabled": true,
"RotationIntervalDays": 90,
"GracePeriodDays": 7,
"CronExpression": "0 0 2 * * ?"
}
}
}

示例中的 Secret 是占位字符串,不能提交真实密钥。NullJwtKeyRotationSink 只记录轮换结果;要自动写入受管密钥源,生产必须覆盖 IJwtKeyRotationSink。

4. 前端通知链接

FrontendLinkBuilder 读取 Frontend:BaseUrl 与路由字典。默认 BaseUrl 是 http://localhost:6800,只适合本地开发。

生产前端链接
{
"Frontend": {
"BaseUrl": "https://portal.example.com",
"Routes": {
"ResetPassword": "/reset-password",
"Activate": "/activate",
"VerifyEmail": "/verify-email",
"ConfirmPhone": "/confirm-phone"
}
}
}

BaseUrl 必须使用 HTTPS、固定可信域名且不带尾斜杠。路由来自服务端配置,不接受请求参数提供任意 ReturnUrl。通知链接中的 Token 应放在前端能安全读取并立刻提交的位置,避免被分析脚本、Referer 或日志收集。

5. 默认实现与失败语义

AddBitzOrcasPlatformIdentity 注册:

  • BCryptPasswordHasher、DefaultPasswordValidator、JwtTokenService。
  • LoginFlow、密码过期与历史服务。
  • 内置 TotpMfaService。
  • AzureAD、WeChat、WeCom、DingTalk 的 UnavailableAuthProvider。
  • UnavailableWeChatMiniProgramAuthService。
  • NullEmailDeliveryPort、NullSmsDeliveryPort。
  • NullLoginLogArchiveQueryPort 和 NullJwtKeyRotationSink。

Unavailable 外部登录应返回稳定不可用错误;Null 投递端口是 best-effort 空实现。两者都不能被健康检查误判为“对应业务已就绪”。

6. 外部身份连接器

调用 AddBitzOrcasIdentityFederationConnectors 后,各连接器独立按条件注册:

连接器注册条件实际适配器
SocialAuth存在 SocialAuth 配置节SocialAuthAdapter
Azure ADAzureAD:Enabled=true 且有 ClientIdAzureAdAdapter,同时注册 Graph、JIT 与 JwtBearer
LDAPLdap:IsEnabled=true 且有 HostLdapAuthAdapter
微信小程序AppId 与 AppSecret 均存在WeChatMiniProgramAdapter,使用独立端口
Azure AD 条件注册的最小形状
{
"AzureAD": {
"Enabled": true,
"Instance": "https://login.microsoftonline.com/",
"TenantId": "organization-tenant-id",
"ClientId": "application-client-id",
"ClientSecret": "injected-secret",
"CallbackPath": "/api/callback/azuread",
"Scopes": ["openid", "profile", "email"]
}
}

Application 与 Contracts 不得引用连接器包。所有 SDK、HttpClient 和供应商错误翻译都留在 Infrastructure。外部身份成功后仍要落入本地账号、租户、角色和会话语义。

6.1 租户级 Provider 管理面

Host 静态配置用于装载协议适配器;租户运行参数和凭据则由管理面维护,不需要重启进程。代码注册的 Profile 定义允许字段、默认值、交互方式与所需 Adapter,租户配置聚合 ExternalLoginProviderConfiguration 只引用稳定 ProfileId。

当前 Profile 目录如下:

ProfileIdProviderKey交互类型必填核心字段只写凭据JIT / 连接测试
azure-adAzureADRedirectTenantId、ClientId、含 openid 的 Scopes、UserIdClaimClientSecret均支持
social-oauth2SocialRedirect公共 HTTPS Authorization/Token/UserInfo 端点、ClientId、Scopes、用户标识字段ClientSecret均支持
ldapLdapCredentialsHost、1–65535 Port、SSL、SearchBaseDn、用户对象类、500–30000ms 超时,以及用户 DN 模板或 UPN 后缀BindPassword;配置 BindDn 时必填均支持
wechat-mini-programWeChatMiniProgramMiniProgramAppIdAppSecret均支持

Social OAuth 端点必须是无 UserInfo、Query 和 Fragment 的公共 HTTPS URL。SAML 已有独立动态配置、元数据校验和运行快照闭环,不在这四个 Profile 中重复建模。

管理 API 与授权

方法与路径行为事务/并发
GET /api/external-login/configurations返回代码 Profile、Adapter 装载状态和当前租户脱敏配置标准读超时
PUT /api/external-login/configurations/{profileId}创建或更新显示名、启用状态、JIT、完整参数与本次凭据变化Version 乐观并发;委托敏感动作
DELETE /api/external-login/configurations/{profileId}?Version={version}按版本软删除租户配置幂等处理不存在目标;委托敏感动作
POST /api/external-login/configurations/{profileId}/test用已保存配置执行真实连接测试INonTransactionalCommand,避免网络 I/O 占用事务

四个用例都要求资源 identity/externallogin 的 Manage 动作,对应权限 identity.externallogin.manage。前端入口位于“集成中心”的外部登录配置 Sheet;UI 只是管理 API 的客户端,不是第二套配置事实源。

保存请求中的 Parameters 是完整非敏感参数;Secrets 只包含本次新增或替换值,ClearedSecretKeys 表示明确清除。未重新提交的已有 Secret 会保留。读取响应只返回 ConfiguredSecretKeys,不会返回凭据值、SecretsJson 或搜索哈希。持久化列由统一 Data Protection 敏感列管道加密;运行对象中的 Secret 不进入响应、日志、异常、缓存键或遥测标签。

启用配置前,服务端同时检查 Profile schema、必填凭据和真实 Adapter 是否已加载。稳定就绪状态包括 Ready、Disabled、Incomplete、AdapterUnavailable 与 NotConfigured。连接测试使用已保存配置,并只返回成功标志、UTC 时间、耗时、稳定错误码和脱敏说明。

运行时每次列举 Provider、发起登录、处理回调、小程序交换或连接测试都会重新读取当前租户的权威 Store,不缓存凭据。配置轮换即时生效;Redirect 流程还用 {configurationId}:{version} 核对运行版本,避免回调继续使用已经变更的旧凭据。明确禁用或删除后,Provider 会从公开登录列表退出,不回退到旧静态配置。

7. MFA 增强连接器

内置 TOTP 在未启用连接器时仍可用。MFA:Enabled=true 后,AddBitzOrcasMfaConnectors 增加 Email/SMS OTP、FIDO2、恢复码、受信设备和风险驱动策略。

连接器默认 MfaCacheProvider 是进程内缓存。非 Development 环境会输出启动警告;多实例生产必须用 Redis 等分布式实现覆盖,否则 FIDO2 Challenge 可能由另一实例接收而无法验证。

NullUserMfaConfigRepository 同样是占位。启用增强 MFA 时,应确认真实用户 MFA 配置仓储、Email/SMS Delivery 和缓存都已注册。

8. 通知适配器

账号激活、密码重置、邮箱确认和手机确认分别消费 IEmailDeliveryPort 或 ISmsDeliveryPort。推荐把供应商状态映射成以下运维语义:

状态用例响应运维动作
已接受投递成功,记录消息标识跟踪后续回执
可重试超时返回失败或进入可靠 Outbox有界重试和告警
地址永久无效类型化业务失败提示修正地址
供应商未配置失败关闭商业流程健康检查标红,禁止 GA

当前 Null 端口可能让调用链成功但不实际送达,因此 GA 门禁必须检查具体实现类型或做端到端投递探针。

9. 权限目录

IdentityPermissions 当前拥有以下权限组:

  • 用户:list、read、create、update、delete、enable、disable、lock、unlock。
  • 角色:list、read、create、update、delete。
  • 登录日志:list、read。
  • MFA:manage。
  • 设备:list、manage。
  • 会话:list、manage。
  • 外部登录:manage。
  • 组织单元:list、read、create、update、delete。

邀请、Admission、Tenant 等 Command 还通过 ResourceDescriptor + AuthorizationAction 表达资源授权。权限目录和资源动作是互补层次,不能只检查其中之一。

10. 敏感配置规则

  • Host 级 JWT 和连接器启动凭据只来自 Secret Store 或环境注入;租户外部登录凭据只经管理 API 写入受保护的 SecretsJson 敏感列。
  • PlatformTenant.ConnectionString 不进入普通配置响应、日志或审计详情。
  • 密码 Hash、SecurityStamp、Raw Token、FIDO2 Challenge 不作为结构化日志字段。
  • 配置快照和诊断端点对敏感字段做固定掩码,而不是保留前后字符。
  • 密钥轮换保留旧验证密钥的宽限期,但不继续使用旧密钥签发。

11. 启动时验证

Terminal window
# 查看 Identity 选项绑定和默认实现。
rg -n "Bind\(|TryAdd|Unavailable|Null.*Port|NullJwt" \
src/Platform/Identity/BitzOrcas.Identity.Application/Identity/DependencyInjection.cs
# 查看连接器的真实条件与覆盖方式。
rg -n "GetSection|GetValue|AddScoped<IExternalAuthProvider|MFA:Enabled" \
src/Platform/Identity/BitzOrcas.Identity.Infrastructure -g '*.cs'
# 配置文件中不应出现已提交的真实 Secret。
rg -n '"(Secret|ClientSecret|AppSecret|Password)"\s*:\s*"[^$<{]' \
src/Hosts -g 'appsettings*.json'

最后一条是启发式扫描,命中后必须人工判断;它不能替代 Secret Scanner。

12. 生产验收表

检查通过证据
密码策略生效弱密码和历史复用集成测试
密钥环可签发与验证新 Kid 签发、旧 Kid 宽限验证、过期拒绝
刷新令牌族存储DI 中为生产 IRefreshTokenStore,重用检测通过
通知真实送达探针消息、供应商回执、失败告警
外部身份可用Profile/Adapter 状态、脱敏配置、连接测试、回调与 JIT/绑定测试
多实例 MFA分布式 Challenge 缓存和跨实例测试
权限目录同步Governance 生成输出和授权契约测试

返回 Identity 总览 · 登录安全 · 测试与运维

100%

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