本页回答三个生产问题:配置键由谁读取、缺省时会发生什么、怎样确认真实适配器已经覆盖占位实现。配置文件能让 Host 启动,不代表身份能力已经可以商业交付。
1. 注册顺序
组合根先注册 Identity 应用服务,再注册 Infrastructure 适配器。TryAdd 默认实现允许后续真实适配器覆盖;顺序错了可能让 Null 或 Unavailable 实现继续生效。
// ① 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:RequireDigit | true | 至少一个数字 |
RequireLowercase | true | 至少一个小写字符 |
RequireUppercase | true | 至少一个大写字符 |
RequireNonAlphanumeric | true | 至少一个符号 |
RequiredLength | 8 | 最小字符数 |
RequiredUniqueChars | 1 | 最少不同字符数 |
Identity:Password:BCrypt:WorkFactor | 12 | BCrypt 成本 |
Identity:Password:Expiry:PasswordExpiryDays | 90 | 0 表示永不过期 |
PasswordExpiryWarningDays | 7 | 提前提醒 |
Identity:Password:History:PasswordHistoryCount | 5 | 历史策略配置值 |
2.1 密码客户端加密 Identity:Password:Cipher
绑定类型:PasswordCipherOptions(节名常量 PasswordCipherOptions.SectionName)。
产品底座 强制 RSA-OAEP-SHA256 客户端加密;无明文 password 回退。未写本节时使用下表默认值即可登录(SDK 会拉 GET /api/auth/cipher-key 并加密)。
| 配置键 | 默认值 | 合法 / 生效范围 | 越界行为 | 作用 |
|---|---|---|---|---|
Identity:Password:Cipher:KeySize | 2048 | 2048–4096 | ValidateOnStart 失败,进程起不来;运行时创建密钥时若仍越界会回落 2048 | RSA 密钥位数;分布式模式保护 PKCS#8 私钥快照,内存降级模式持有进程内 RSA |
RotationHours | 24 | 1–720(上限 720 小时 = 30 天) | 不做启动校验;CipherKeyRotationHostedService 用 Math.Clamp(..., 1, 24*30) 静默钳制 | 后台密钥轮换周期(小时)。写 0 会被当成 1;写 1000 会被当成 720 |
GracePeriodMinutes | 10 | 运行时下限 1 分钟;无启动上限校验 | Math.Max(1, GracePeriodMinutes);过小/负数变 1,过大不会被拒绝 | 旧密钥仍可解密的宽限窗口(分钟)。应 远小于 轮换周期;推荐 5–30。客户端遇 KeyExpired 时 SDK 会刷新公钥重试一次 |
MaxClockSkewSeconds | 120 | 30–600(10 分钟) | ValidateOnStart 失败 | 载荷时间戳相对服务端时钟允许偏差;同时经 cipher-key 响应下发给客户端做时钟校准 |
NonceTtlSeconds | 120 | 30–600(10 分钟) | ValidateOnStart 失败;领取 Nonce 时再 Clamp(30, 600) | Nonce 一次性缓存 TTL(秒);防重放 |
{ "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:Issuer | BitzOrcas | 与验证端一致 |
Jwt:Audience | BitzOrcas | 与 API 资源一致 |
Jwt:Secret | 空 | 至少 32 字符,只能来自秘密管理系统 |
Jwt:AccessTokenExpirationMinutes | 15 | 缩短会增加刷新压力,延长会扩大泄露窗口 |
Jwt:RefreshTokenExpirationDays | 7 | 与 Session 默认 7 天语义对齐 |
开发环境可使用单一 Secret;生产优先配置带 Kid 的 SigningKeys,让签发选择当前密钥、验证接受宽限期内的旧密钥。
{ "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 AD | AzureAD:Enabled=true 且有 ClientId | AzureAdAdapter,同时注册 Graph、JIT 与 JwtBearer |
| LDAP | Ldap:IsEnabled=true 且有 Host | LdapAuthAdapter |
| 微信小程序 | AppId 与 AppSecret 均存在 | WeChatMiniProgramAdapter,使用独立端口 |
{ "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 目录如下:
| ProfileId | ProviderKey | 交互类型 | 必填核心字段 | 只写凭据 | JIT / 连接测试 |
|---|---|---|---|---|---|
azure-ad | AzureAD | Redirect | TenantId、ClientId、含 openid 的 Scopes、UserIdClaim | ClientSecret | 均支持 |
social-oauth2 | Social | Redirect | 公共 HTTPS Authorization/Token/UserInfo 端点、ClientId、Scopes、用户标识字段 | ClientSecret | 均支持 |
ldap | Ldap | Credentials | Host、1–65535 Port、SSL、SearchBaseDn、用户对象类、500–30000ms 超时,以及用户 DN 模板或 UPN 后缀 | BindPassword;配置 BindDn 时必填 | 均支持 |
wechat-mini-program | WeChatMiniProgram | MiniProgram | AppId | AppSecret | 均支持 |
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. 启动时验证
# 查看 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 总览 · 登录安全 · 测试与运维