Skip to content
bitzorcas
中EN

Guide

在线许可证签发与分发

配置厂商许可证控制面、独立签名宿主与 Azure KMS/HSM,通过前端完成四眼审批、异步签发、下载、分发和吊销。

Last updated

可以在前端完成在线签发工作流,但浏览器不执行密码学签名。前端只调用受权控制面 API;私钥始终留在 Azure Key Vault 或 Managed HSM,独立 BitzOrcas.LicenseSigner 宿主只请求 KMS/HSM 完成 ES256 签名。

当前已经实现的边界

受权管理员前端

LicenseManagement 控制面 API

申请、审批、操作租约

后台签名工作器

独立 LicenseSigner

Azure Key Vault / Managed HSM

持久幂等回执

签名 Envelope + SHA-256 下载

当前“在线签发”包括:

  • 前端创建申请、审批/驳回、触发签发、查看后台状态、下载和吊销;
  • 没有客户 Runtime License 时,最低身份会话、首次登录强制改密、密码恢复、按权限过滤的应用导航和受权限保护的 LicenseManagement 控制面仍可运行,避免首次签发循环依赖;
  • API 以持久租约异步调用独立签名宿主;
  • 签名宿主使用版本化 Azure 密钥 URI;
  • API 用独立配置的可信公钥复验签名,并逐字段确认签名宿主只增加了 signature;
  • 同一幂等操作可在网络或进程故障后安全恢复。

当前不包括客户匿名自助签发、浏览器持有私钥、前端直连 KMS/HSM,也不包括一个公开的客户在线 Lease 下载服务。正式 Envelope 目前由受权控制面下载,再经受控渠道分发;Runtime 也支持另行实现 ILicenseLeaseSource 的在线续租 Adapter。

1. 准备 Azure 密钥

为每次轮换创建一个不可变密钥版本,并保存三项信息:

项目示例是否保密
KeyIdkey-2026-01否,稳定协议标识
版本化密钥 URIhttps://vault.managedhsm.azure.net/keys/license-key/<version>否,但应受控
ES256 公钥 PEM-----BEGIN PUBLIC KEY-----…否,分发给验证端

URI 必须包含具体版本;不能使用隐式 latest。签名宿主运行身份只授予读取密钥元数据和签名所需的最小权限,不能导出私钥。API 使用的公钥必须通过独立可信渠道取得,不能把 KMS 返回值当作唯一信任来源。

本地第一次创建 P-256 ES256 密钥可以使用 Azure CLI:

Terminal window
az login
az keyvault key create \
--vault-name <vault-name> \
--name bitzorcas-runtime-license \
--kty EC \
--curve P-256 \
--ops sign verify
# 保存输出的完整、版本化 kid。
az keyvault key show \
--vault-name <vault-name> \
--name bitzorcas-runtime-license \
--query key.kid \
--output tsv
# <version> 是上一步 kid 的最后一段。
az keyvault key download \
--vault-name <vault-name> \
--name bitzorcas-runtime-license \
--version <version> \
--encoding PEM \
--file bitzorcas-runtime-license-public.pem

Key Vault 数据面优先使用 Azure RBAC。本地运行 LicenseSigner 的 Azure CLI 身份可授予 Key Vault Crypto User,生产改用独立 Managed Identity;创建/轮换密钥的管理身份与日常签名身份应分离。参数与身份模型可核对 Microsoft Learn:az keyvault key 和 Microsoft Learn:Key Vault authentication。

2. 配置本地 AppHost

先完成本地基础设施中的 SQL Server、RabbitMQ、Redis、JWT 和 AppHost 参数。然后把开发签名配置写入 AppHost user-secrets:

Terminal window
# 固定控制面使用的协议 KeyId 和不可变 KMS 密钥版本。
dotnet user-secrets set \
"LicenseManagement:Development:KeyId" \
"key-2026-01" \
--project src/Hosts/BitzOrcas.AppHost
dotnet user-secrets set \
"LicenseManagement:Development:VersionedKeyUri" \
"https://<vault>.managedhsm.azure.net/keys/<name>/<version>" \
--project src/Hosts/BitzOrcas.AppHost
# 从独立可信渠道配置对应公钥,并显式开启后台工作器。
dotnet user-secrets set \
"LicenseManagement:Development:TrustedPublicKeyPem" \
"$(cat bitzorcas-runtime-license-public.pem)" \
--project src/Hosts/BitzOrcas.AppHost
dotnet user-secrets set \
"LicenseManagement:Development:SigningWorkerEnabled" \
"true" \
--project src/Hosts/BitzOrcas.AppHost

如需覆盖开发控制面租户:

Terminal window
# 生产环境必须换成独立的厂商控制面租户。
dotnet user-secrets set \
"LicenseManagement:Development:ControlPlaneTenantId" \
"1000001" \
--project src/Hosts/BitzOrcas.AppHost

AppHost 会生成并持久化 API 与签名宿主共用的开发服务凭据,不需要开发者复制 Token。DefaultAzureCredential 已禁用交互式浏览器回退;本地可使用已登录的 Azure CLI/IDE 身份,生产使用工作负载身份。

3. 启动并检查

Terminal window
scripts/local/bootstrap.sh aspire
scripts/local/doctor.sh aspire
dotnet run --project src/Hosts/BitzOrcas.AppHost

doctor.sh aspire 不打印密钥或密码,只报告签发端是否缺少版本化密钥 URI、可信公钥、工作器开关,以及 本机 DeploymentId 和 Runtime License Envelope 是否已经存在。缺少签发端配置只会让在线签发失败关闭; 缺少 Envelope 时,community.small.v1 在人数不超过 30 的边界内继续运行;未选择该策略、计量不可用或已经超限时,普通业务才会被 Runtime License 拦截。

如果这是空的 Development 演示库,需要 admin/operator 账号验证四眼流程,请把启动命令改为:

Terminal window
BITZORCAS_ASPIRE_SEED_DEMO=true dotnet run --project src/Hosts/BitzOrcas.AppHost

前置 bootstrap.sh aspire 会在 secret 缺失时为十个演示账号生成一个仅限本机 Development 的 10 字符 初始密码,并把登录提示写到被 gitignore 的 .bitzorcas/demo-credentials(权限 0600)。 默认不强制首次改密。另开终端查看:

Terminal window
cat .bitzorcas/demo-credentials

使用其中的租户 1000001、admin 和 operator 登录。初始明文只会注入一次性 schema initializer, 写库前立即转换成 BCrypt,常驻 API/JobHost 不接收该值;重复 seed 也不会覆盖已经修改过的账号密码。 重启 AppHost 不会轮换该 secret。旧 44 字符值的轮换步骤见 演示数据与种子参考。 显式 AppHost demo seed 只在该一次性进程中用高优先级空白配置遮蔽 API user-secrets 里可能遗留的 USER:*:PASSWORD_HASH,避免旧哈希覆盖新凭据提示;原 secret 不会被删除, 非 Aspire seed 仍优先使用预计算哈希。

用户角色 CSV 使用可读的 UserName 作为种子输入。写入授权表前,Identity adapter 会把用户名解析为 不可变用户 ID,并一次性校验所有用户和角色引用;任一引用无效时整步拒绝写入,不会留下部分角色绑定。

AppHost 会按顺序完成业务 schema、Quartz schema,再启动 API 和 JobHost。它同时给 API/JobHost 注入正式 Runtime adapter,使用同一部署身份和离线信封:

.bitzorcas/license/deployment-id
.bitzorcas/license/runtime-license.json

API 第一次启动会原子创建 DeploymentId。另开终端读取:

Terminal window
cat .bitzorcas/license/deployment-id

然后检查:

  1. Dashboard 中 schema-initializer 和 quartz-schema-initializer 成功退出;
  2. api、jobhost 和 license-signer 处于 Running;
  3. 使用拥有 license-management.runtime-license.view 权限的账号打开签名网关状态;
  4. 状态为 ready 后再触发签发。

/health/live 只证明签名宿主进程存活。/health/ready 还会验证配置、KMS 和回执目录,并要求服务间 Bearer;日常使用控制面“签名网关状态”查看,不要把 Bearer 放进浏览器或手工 curl 历史。

4. 在前端完成四眼工作流

管理页面:

/license-management/runtime-licenses

该页面已经在仓库 frontend/apps/app 中实现,并通过 Platform SDK 调用下列控制面 API;它不是待开发的 概念页。首次登录若要求改密,应先完成改密再进入该页面。没有 Runtime License 时,未认证调用会得到 Authentication.Required,已认证但权限不足会得到授权拒绝;不应先被 Licensing.Runtime.Unavailable 拦截。

显式执行 Development demo seed 后,控制面默认绑定租户 1000001:

  • admin / root-admin:创建、签发、下载、吊销;
  • operator:查看、审批、驳回。

完整流程:

  1. admin 创建申请;
  2. operator 审批;申请人与审批人不能相同;
  3. admin 触发签发;
  4. 页面观察 Pending → Processing → Completed;
  5. 状态成为 Issued 后下载 Envelope,并核对页面显示的 SHA-256;
  6. 经受控渠道交付给客户。

若申请不合格,operator 填写原因后驳回,状态进入 Rejected,不能在原记录上修改后继续。应重新创建申请。

5. 填写申请

字段操作要求
CustomerId客户稳定标识,不是控制面租户
ProductId必须在签名宿主 AllowedProductIds 中
Edition / Features与合同和交付 Profile 一致
VersionRange明确的允许版本范围
TenancyModes / Environments只包含实际授权范围
DeploymentLimit1 到 100000
NotBefore / ExpiresAt业务有效窗口
OfflineUntil / GraceUntil满足 NotBefore ≤ OfflineUntil ≤ GraceUntil
DeploymentId客户持久部署身份;不能使用 Pod/MAC/CPU
KeyId与版本化 KMS URI和可信公钥目录完全一致

Protocol v1 用于专属部署。Protocol v2 当前只允许 community.web.v1,前端会固定其 Profile、Composition 和共享 DeploymentId;后端仍做最终校验。

完整产品本地调试的推荐值

字段值
ProductIdbitzorcas-modern
EditionEnterprise
Featuresframework.core, framework.aspnetcore, framework.infrastructure, workflow.runtime
VersionRange[1.0.0,2.0.0)
TenancyModesmulti-tenant
Environmentsdevelopment
DeploymentLimit1
DeploymentId.bitzorcas/license/deployment-id 的文件内容
KeyIdAppHost 配置的稳定别名,例如 key-2026-01
ProtocolVersion1

时间窗口至少满足 NotBefore ≤ ExpiresAt、NotBefore ≤ OfflineUntil ≤ GraceUntil,并覆盖计划调试周期。申请提交后载荷不可修改;填错时应重新创建。

如何取得客户 DeploymentId

客户先配置一个持久 Licensing:Runtime:DeploymentIdentityPath。Runtime 首次访问该存储时会原子创建 32 位小写 GUID 文本。客户把该 ID 通过受控渠道提供给签发方;文件本身必须保留在 PV/持久 Secret 中。

不要在签发后删除或重新生成该文件,否则许可证会因部署身份不匹配而变为 Invalid。

6. 后台签发与重试

“签发”按钮不会同步等待 KMS。API 先在短事务中冻结请求和幂等事实,后台工作器再:

  1. 领取数据库租约;
  2. 在事务外调用签名宿主;
  3. 取得或恢复持久签名回执;
  4. 用可信公钥复验并比对完整载荷;
  5. 在新事务中提交 Envelope。

暂时故障按有界退避自动重试。连续八次仍失败或持久载荷损坏时进入 Failed,页面显示脱敏稳定错误码。排除 KMS、网络、回执卷或配置问题后,由操作者重新确认并排队;不要直接改数据库状态。

7. 生产配置

API/控制面:

LicenseManagement:ControlPlane:TenantId=<独立厂商控制面租户>
LicenseManagement:Signing:BaseUrl=https://license-signer.internal
LicenseManagement:Signing:SignPath=/v1/licenses/sign
LicenseManagement:Signing:ReadinessPath=/health/ready
LicenseManagement:Signing:Provider=production-kms
LicenseManagement:Signing:TimeoutSeconds=10
LicenseManagement:Signing:TrustedPublicKeys:<key-id>=<PEM 公钥>
LicenseManagement:Signing:BearerToken=<短期服务凭据>
LicenseManagement:SigningWorker:Enabled=true
LicenseManagement:SigningWorker:PollIntervalSeconds=5
LicenseManagement:SigningWorker:BatchSize=10
LicenseManagement:SigningWorker:LeaseSeconds=90

独立签名宿主:

LicenseSigner:AllowedProductIds:0=bitzorcas-modern
LicenseSigner:Authentication:BearerToken=<与 API 当前值一致>
LicenseSigner:Authentication:PreviousBearerToken=<仅轮换窗口可选>
LicenseSigner:AzureKeyVault:Keys:<key-id>=https://<vault>/keys/<name>/<version>
LicenseSigner:ReceiptStore:Directory=/var/lib/bitzorcas/license-signing-receipts

生产要求:

  • API、签名宿主和 KMS 位于受控私有网络;
  • 控制面租户不能沿用普通业务租户;
  • 前端权限按创建、审批、签发、下载、吊销拆分;
  • 回执目录使用专用持久卷;多副本共享支持原子创建的存储,否则限制为单副本;
  • Bearer、工作负载身份和 KMS 审计进入 Secret/安全运维系统;
  • 私钥、Bearer 和签名请求原文不进入日志。

8. Bearer 与密钥轮换

服务凭据轮换顺序:

  1. 签名宿主配置新 BearerToken,旧值暂放 PreviousBearerToken;
  2. 更新全部 API 实例,只发送新值;
  3. 确认旧值不再使用;
  4. 删除 PreviousBearerToken。

签名密钥轮换顺序:

  1. 创建新不可变密钥版本和新 KeyId;
  2. 先向 API/Runtime 分发新公钥;
  3. 确认所有目标实例信任新 KeyId;
  4. 开始用新 KeyId 签发;
  5. 等旧 License 完成换发或合同窗口结束后再移除旧公钥。

9. 下载与客户启用

下载结果包含建议文件名、JSON Envelope 和 SHA-256。通过认证门户、企业文件交换或等价受控渠道交付;不要把 License 原文贴进公开工单。

本仓库本地 AppHost 的激活方式:

Terminal window
mkdir -p .bitzorcas/license
cp <下载的许可证文件> .bitzorcas/license/runtime-license.json

随后重启 API/JobHost。Runtime 只在启动时导入显式离线文件,再将通过验签的信封写入各自缓存;单纯下载但不放入固定路径不会激活。直接 dotnet run API 或 JobHost 时,Development appsettings 已使用同一仓库相对路径,但仍需在对应 Host 的 user-secrets/环境变量中提供 Licensing:Runtime:TrustedPublicKeys:<key-id>。

客户 Runtime 最小配置:

Licensing:Runtime:Enabled=true
Licensing:Runtime:ProductId=bitzorcas-modern
Licensing:Runtime:ProductVersion=<部署版本>
Licensing:Runtime:Environment=<授权环境>
Licensing:Runtime:TenancyMode=<授权租户模式>
Licensing:Runtime:DeploymentIdentityPath=/var/lib/bitzorcas/license/deployment-id
Licensing:Runtime:CachePath=/var/lib/bitzorcas/license/runtime-license.json
Licensing:Runtime:OfflineLicensePath=/run/secrets/bitzorcas-license.json
Licensing:Runtime:TrustedPublicKeys:<key-id>=<PEM 公钥>

启动后检查 /health/license。只有签名、上下文、DeploymentId 和时间窗口全部匹配才会进入 Valid;复制文件不等于激活成功。

10. 吊销

只有拥有 license-management.runtime-license.revoke 权限的操作者可以吊销。填写原因并确认后,控制面通过同一 KMS 重新签发 Revoked=true 的可验证 Envelope。将该撤销结果交付到目标 Runtime 或在线 Lease Source 后,readiness 变为 Unhealthy,业务执行失败关闭。

删除数据库记录或客户本地文件不是可验证吊销。

故障速查

状态/错误处理
license-signer.not-configured检查 AllowedProductIds、Bearer、版本化 URI、回执目录
provider-unavailable检查工作负载身份、KMS 网络和 get/sign 权限
receipt-store-unavailable检查专用卷、权限和多副本共享语义
invalid-request检查 KeyId、排序集合、时间轴、协议组合和部署上限
idempotency-conflict同一操作键绑定了不同载荷;停止重试并调查调用方
操作长期 RetryScheduled检查网关 readiness、工作器开关和下次重试时间
Runtime Invalid核对公钥、产品/版本/环境/tenancy、DeploymentId 和文件完整性

另见

100%

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