Enterprise 版本
SaaS — 深度参考
Enterprise SaaS 模块为基于 NextPDF 的服务提供多租户构建块。
TenantContext是一个不可变的身份值对象,仅从经过认证的上下文解析而来。ApiKeyGenerator与ApiKeyAuthenticator签发并验证带前缀、带校验和、以哈希存储的 API key。QuotaChecker按每租户配额对请求进行门控:80% 时告警,100% 时拒绝,用量未知时以失败关闭方式拒绝。SidecarJwtMinter为组件间调用铸造短期的 HS256 服务 token。UsageMeter与StripeMeteringSyncer拉取用量事件,并以确定性幂等的方式将其同步至计费提供方。
可用性与授权
标题为“可用性与授权”的章节此能力随 NextPDF Enterprise(nextpdf/enterprise)一同交付,并通过 Enterprise 层级的授权信封(license envelope)激活。缺少该授权的部署不会加载此能力的类。比较版本并获取授权。
SaaS 范围是一项 Enterprise 基础能力;不存在单独的按功能划分的标志。NextPDF Core(Apache-2.0)与 NextPDF Pro 没有任何租户、API-key 或配额模型;此能力在更低层级没有对应物。
composer require nextpdf/enterprise:^3公开 API 界面
标题为“公开 API 界面”的章节所有符号位于 NextPDF\Enterprise\SaaS 下。
| 符号 | 参数 | 默认行为 | 返回 | 抛出或失败于 | 备注 |
|---|---|---|---|---|---|
TenantContext | string $tenantId, string $source, array $scopes = ['read'] | 不可变的身份值对象 | 值对象 | 无 | 来源:jwt、mtls、api_key;hasScope() / hasAnyScope() 检测 scope |
TenantContext::singleTenant() | 无 | 固定的 default 租户,具有 read、write、admin | TenantContext | 无 | 单租户部署 |
ApiKeyAuthenticator::authenticate() | string $rawKey | 六步验证,随后解析上下文 | TenantContext | ApiKeyAuthenticationException(HTTP 401) | 上下文 source 为 api_key;scope 从 key 记录复制而来 |
ApiKeyAuthenticator::requireScope() | TenantContext $context, ApiKeyScope $requiredScope | 显式的 scope 断言 | void | ApiKeyAuthenticationException::insufficientScope()(HTTP 403) | scope 强制是一个独立的显式步骤 |
ApiKeyGenerator::generateLive() / ::generateTest() | 无 | 新 key:前缀、32 字符 base62 主体(192 位熵)、4 字符校验和 | array{key, hash, prefix} | 无 | 前缀 npf_live_ / npf_test_;hash 为存储摘要 |
ApiKeyGenerator::validateChecksum() | string $key | 前缀、长度与 CRC32 校验和的形态检查 | bool | 无 | 在任何数据存储查询之前防笔误;不是安全控制 |
ApiKeyGenerator::hashKey() (static) | string $key | 原始 key 的 SHA-256 十六进制摘要 | string | 无 | key 唯一被存储的表示 |
ApiKeyGenerator::isLiveKey() / ::isTestKey() | string $key | 前缀检查 | bool | 无 | 无需查询即可看出所处环境 |
ApiKey | id, tenant, key hash, display prefix, scope mask, created/expires/revoked instants | 已存储的 key 记录;明文绝不持久化 | 值对象 | 无 | isActive(), isRevoked(), isExpired(), scopeNames() |
ApiKeyScope | backed enum: Read = 1, Write = 2, Admin = 4 | 位掩码 scope 模型 | 枚举 | 无 | maskFromNames(), fromName(), fullAccess();掩码构建器会忽略未知名称 |
ApiKeyRepositoryInterface | — | 存储契约;仅以哈希持久化 | — | 由实现定义 | findByHash(), findActiveByTenant(), store(), revoke() |
SidecarJwtMinter::__construct() | string $secret, issuer, audience, int $ttlSeconds = 300 | 构造时拒绝小于 16 字节的签名密钥 | 实例 | InvalidArgumentException | 128 位密钥强度下限;建议 32 字节或更多的随机字节 |
SidecarJwtMinter::mint() | TenantContext $tenant | 携带 iss, aud, sub, scope, tenant_id, iat, exp, jti 的 HS256 JWT | string | claim 编码失败时抛出 JsonException | 默认五分钟生命周期;jti 为 16 个随机字节的十六进制编码 |
QuotaChecker::check() | TenantContext $tenant, TenantQuota $quota | 读取当前用量;80% 时告警;100% 时拒绝;用量未知时拒绝 | array{allowed: bool, warning_percentage: float|null} | QuotaExceededException, QuotaUnavailableException | 两个阈值处都会调用告警回调 |
TenantQuota | float $maxCuPerPeriod, collections, storage bytes, concurrent jobs | 按周期的限额;80% 软阈值常量 | 值对象 | 无 | fromConfig() 默认值:10,000 CU、100 collections、10 GB、10 jobs |
QuotaExceededException::toErrorEnvelope() | 无 | SPEC-QUOTA-001 错误信封 | array | — | HTTP 402,不可重试;携带当前值、限额与重置时刻 |
QuotaUnavailableException::toErrorEnvelope() | 无 | SPEC-QUOTA-503 错误信封 | array | — | HTTP 503,可重试;原因 usage_undeterminable |
UsageMeter::pullUsage() | array<string, int> $watermarks | 从各自游标处轮询每个已配置的用量来源主机 | array{events, instance_id} | 每个主机都不可达时抛出 UsageMeterException | 容忍部分中断;不可达的主机会被记录并跳过 |
UsageMeter::getCurrentUsage() | string $tenantId | 当期的计算单元用量 | float | 用量无法确定时抛出 UsageMeterException | 可解析的零是权威的;用量未知则抛出 |
StripeMeteringSyncer::sync() | array<string, int> $watermarks | 一个拉取、转换、发送的周期 | array{watermarks, sent, failed} | 无;发送失败会路由到 DLQ 回调 | 拉取失败返回一个保留游标的无操作周期 |
StripeAdapter::sendMeterEvent() | MeterEvent $event | 携带幂等 header 向提供方 POST | void | StripeSyncException | HTTP 429 与 5xx 可重试;其他 4xx 不可重试 |
StripeAdapter::sendBatch() | list<MeterEvent> $events | 发送每个事件;收集失败项 | list<StripeSyncException> | 无 | 空列表意味着每个事件都成功 |
MeterEvent | meter name, tenant, value, idempotency key, timestamp | 不可变的计量事件值对象 | 值对象 | 无 | toStripePayload() 序列化提供方载荷 |
final readonly class ApiKeyAuthenticator{ public function __construct( private ApiKeyRepositoryInterface $repository, private ApiKeyGenerator $generator, private LoggerInterface $logger, ) {}
public function authenticate(string $rawKey): TenantContext {}
public function requireScope(TenantContext $context, ApiKeyScope $requiredScope): void {}}final class QuotaChecker{ public function __construct( private readonly UsageMeterInterface $usageMeter, private readonly LoggerInterface $logger, private readonly Closure $quotaAlertCallback, ) {}
/** @return array{allowed: bool, warning_percentage: float|null} */ public function check(TenantContext $tenant, TenantQuota $quota): array {}}interface UsageMeterInterface{ /** @return array<string, mixed> */ public function pullUsage(array $watermarks): array;
public function getCurrentUsage(string $tenantId): float;}final class StripeMeteringSyncer{ public function __construct( private readonly UsageMeterInterface $usageMeter, private readonly StripeAdapterInterface $stripeAdapter, private readonly LoggerInterface $logger, private readonly Closure $dlqCallback, ) {}
/** @return array{watermarks: array<string, int>, sent: int, failed: int} */ public function sync(array $watermarks): array {}}final readonly class SidecarJwtMinter{ public function __construct( private string $secret, private string $issuer = 'nextpdf-enterprise', private string $audience = 'nextpdf-spectrum', private int $ttlSeconds = self::DEFAULT_TTL_SECONDS, ) {}
public function mint(TenantContext $tenant): string {}}行为契约
标题为“行为契约”的章节- 租户身份。 租户上下文是不可变的:租户标识符、解析来源、scope。身份仅从经过认证的上下文解析(
jwt、mtls、api_key)——绝不从客户端提供的 header 或查询参数解析。单租户部署使用具有完整 scope 的固定default上下文。 - 认证顺序。 API-key 认证按固定顺序进行:校验和、SHA-256 哈希、仓库查询、吊销检查、过期检查、上下文解析。未知、已吊销与已过期的 key 是三种独立结果,均为 HTTP 401;scope 不足则为 HTTP 403。
- key 保密性。 原始 key 绝不被存储或记录到日志;只有其 SHA-256 摘要被持久化并用于查询。认证器本身不执行任何逐字节的密钥比较;时序恒定的摘要查询是仓库实现的契约。
- 配额阈值。 在 80% 软限制处,请求继续进行,返回告警百分比,并触发告警回调。在 100% 硬限制处,请求以携带重置时刻的
SPEC-QUOTA-001(HTTP 402)被拒绝——重置时刻为下个月第一天的 UTC 午夜。 - 配额失败关闭。 无法确定的用量会以
SPEC-QUOTA-503(HTTP 503,可重试)拒绝请求。用量未知绝不被当作零处理。真正可解析的零用量是权威的,并予以放行。 - 告警去重。 检查器不对告警去重;按周期去重是回调的职责。
- 计量同步。 该周期是计划执行的,绝不在请求路径上。它从每来源水位处恢复,并将每个游标推进到成功发送的最高事件标识。幂等键是确定性的——租户、周期、事件标识——因此重发的事件会在提供方的去重中被折叠。
- 拉取失败。 失败的拉取返回一个无操作周期(
sent为 0,failed为 0)并保留水位;下一个周期会重试同一窗口而非跳过它。 - 服务 token。 token 使用带共享密钥的 HS256,并携带
iss、aud、sub、scope、tenant_id、iat、exp以及一个唯一的jti。默认生命周期为五分钟。构造会以失败关闭方式拒绝小于 16 字节的密钥。
边界情形与失败模式
标题为“边界情形与失败模式”的章节- 格式错误的 key 未通过校验和,会在任何数据存储访问之前被拒绝。格式正确但未知的 key 会在查询之后被拒绝。两者都表现为无效 key 结果。
- 未知、已吊销与已过期的 key 使用不同的异常工厂;
keyExpired标志仅在已过期结果中为 true。请将它们映射到不同的客户端响应。 QuotaChecker::check()仅在放行时返回;返回的allowed始终为true。拒绝与不可用属于异常结果。- 对于非正配额,
TenantQuota::usagePercentage()返回0.0;fromConfig()会用默认值替换缺失的值,并将整数限额下限钳制为至少 1。 - 水位是按来源划分的;缺失的水位从该来源数据流的起点(游标
0)开始。多来源部署维护各自独立的水位。 - 转换会跳过非数组事件、operation 或 tenant 缺失或为空的事件、值非正的事件,或 operation 未映射的事件——而不令整个周期失败。缺少可用的正整数标识的事件会被拒绝并附带一条告警:随机的回退键会破坏提供方侧的去重,并可能对该租户重复计费。
- 连续十次发送失败会升级为一条 critical 日志条目;任何一次成功发送都会重置计数器。每个失败的事件仍会到达死信回调。
- 来自用量来源主机的格式错误的 JSON 主体会产生一个空的事件列表,而非导致周期失败。仅当每个已配置的主机都不可达时,
pullUsage()才抛出。
FIPS-mode 行为
标题为“FIPS-mode 行为”的章节- 摘要与 MAC 原语是通过宿主 PHP 密码学提供方使用的 SHA-256 与 HMAC-SHA256。受 FIPS 约束的构建在遇到非批准算法时会以失败关闭方式处理而非降级;SaaS 层不附加自身的任何密码学策略。
- key 主体与 token 标识符来自 CSPRNG(
random_int()、random_bytes())。 - CRC32 校验和不是密码学控制,不受 FIPS 模式影响。
符合性
标题为“符合性”的章节以下陈述描述的是相对于所引用条款的能力。它们不是认证声明;NextPDF 未持有此模块的任何认证。
| 行为 | 引用 |
|---|---|
服务 token 的 exp not-after 语义 | RFC 7519 §4.1.4 |
| 服务 token 的 JWS compact serialization | RFC 7515 §3.1 |
| 16 字节 HS256 密钥下限;不以人类可记忆的口令作为 MAC 密钥 | RFC 8725 §3.5 (threat: §2.2) |
| 仓库摘要查询的时序恒定契约 | OWASP ASVS 5.0 §11.2.4 |
| API-key 存储摘要 SHA-256 | FIPS 180-4 (code-declared) |
RFC 8725 与 OWASP ASVS 5.0 的引用经过 RAG 验证;完整的 reference identifier 记录在本页的 frontmatter 中。FIPS 180-4、FIPS 198-1 与 BSI TR-02102-1 的引用是在产品源码中以代码声明的(hash('sha256', …) 及铸造器所记录的密钥下限);本页并未从 RAG 语料库检索它们。ASVS §11.2.4 的时序恒定要求约束的是运营方提供的仓库实现,而非认证器类本身。
开发说明
标题为“开发说明”的章节- 请提供
ApiKeyRepositoryInterface与StripeAdapterInterface的持久化实现;该包交付的是契约与一个 PSR-18 提供方客户端,而非持久化。 - 依赖仅为 PSR 抽象:PSR-3 logger、PSR-18 HTTP client、PSR-17 request 与 stream 工厂。无需任何提供方 SDK。
- 将计量同步作为计划任务运行。在每个周期之后持久化返回的水位。
- 将配额告警百分比呈现给客户端,例如作为一个 warning header,并在回调中按周期对配额告警去重。
- 从配置中以高熵随机值提供 token 铸造器的密钥;建议使用 32 字节或更多的随机字节。绝不从口令派生它。
- key 前缀使得无需查询即可看出所处环境;sandbox 与 production 的 key 绝不会冲突,因为前缀参与了所存储的摘要。
- 内部机制细节保留在源码仓库的内部文档中,不在本手册范围之内。
发布边界
标题为“发布边界”的章节本页仅记录外部可观察的行为与受支持的公开 API 界面。内部命名空间路径、辅助类、机制表、runbook 文件名与工单前缀均不在范围之内。