SpacetimeDB HTTP API 授权机制完全指南:JWT 身份认证、Bearer 令牌与匿名访问详解
【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB
本篇指南以 SpacetimeDB 官方参考文档 HTTP API 授权(Authorization) 为核心脉络,深入讲解 SpacetimeDB 如何通过 OpenID Connect 兼容的 JSON Web Token(JWT)构建身份与授权体系。你将掌握:身份(Identity)如何从 JWT 的sub/iss声明派生、Authorization: Bearer请求头的正确用法、匿名访问的权限边界,以及GET /v1/ping等顶层路由的用途。同时结合仓库源码,我们还会追溯令牌签发与验证的底层实现,帮助你从"会调接口"进阶到"理解机制"。
SpacetimeDB 的授权模型:Identity 与 Token 的二元结构
SpacetimeDB 的授权体系建立在两个核心概念之上:身份(Identity)与令牌(Token)。
- 身份是用户在 SpacetimeDB 中的全局唯一标识,也是数据库所有权(owner)、行级安全策略(row-level security)等权限判定的依据;
- 令牌是身份的"凭证",客户端通过 HTTP 请求头携带令牌,服务端校验令牌后即可确定调用者的身份。
关键的设计约束在于:SpacetimeDB 可以从任何 OpenID Connect 兼容的 JWT 中派生身份,具体做法是从 JWT 的sub(subject,主题)与iss(issuer,签发者)两个声明中计算得出。这一点在 crates/lib/src/identity.rs 中有着明确的实现——Identity::from_claims将"{issuer}|{subject}"作为输入,经过blake3哈希并裁剪、附加校验字节后得到最终的 32 字节身份值:
/// Derives an identity from a [JWT] `issuer` and a `subject`. pub fn from_claims(issuer: &str, subject: &str) -> Self { let input = format!("{issuer}|{subject}"); let first_hash = blake3::hash(input.as_bytes()); let id_hash = &first_hash.as_bytes()[..26]; let mut checksum_input = [0u8; 28]; checksum_input[2..].copy_from_slice(id_hash); checksum_input[0] = 0xc2; checksum_input[1] = 0x00; let checksum_hash = &blake3::hash(&checksum_input); // ...组装最终 Identity }这意味着:只要给出相同的iss与sub,无论令牌由谁签发,派生出的身份都是确定且一致的。这是 SpacetimeDB 能同时支持"自签令牌"与"第三方 OIDC 令牌"两种接入方式的根本原因。
身份与令牌的三种生成途径
根据授权文档,SpacetimeDB 客户端可以通过以下三种方式获得身份与令牌:
1. 通过POST /v1/identity端点申请
客户端可以向 SpacetimeDB 主机的 POST /v1/identity 端点发起请求,申请一个由 SpacetimeDB 主机私钥签名的全新身份与令牌。响应的 JSON 形式为:
{ "identity": string, "token": string }需要特别强调的是,这种令牌不可移植到其他 SpacetimeDB 集群——它由当前集群的私钥签名,其他集群无法验证其签名(这一点我们会在"验证链路"一节详细展开)。
在源码 crates/client-api/src/routes/identity.rs 中,create_identity处理函数会调用SpacetimeAuth::alloc完成签发,最终返回CreateIdentityResponse { identity, token }。而SpacetimeAuth::alloc的实现位于 crates/client-api/src/auth.rs:
/// Allocate a new identity, and mint a new token for it. pub async fn alloc(ctx: &(impl NodeDelegate + ControlStateDelegate + ?Sized)) -> axum::response::Result<Self> { // Generate claims with a random subject. let subject = Uuid::new_v4().to_string().into(); let claims = TokenClaims { issuer: ctx.jwt_auth_provider().local_issuer().into(), subject, // Placeholder audience. audience: ["spacetimedb".into()].into(), extra: None, }; let (claims, token) = claims.encode_and_sign(ctx.jwt_auth_provider()).map_err(log_and_500)?; // ... }从源码可以看到,新身份的sub声明是一个随机生成的 UUID v4 字符串,iss则是当前集群的local_issuer,aud是占位用的"spacetimedb"。之后用 ES256 算法签名生成 JWT。
2. 通过 WebSocket API 匿名连接自动生成
客户端通过 WebSocket API 发起匿名连接时,SpacetimeDB 同样会自动生成一个新的身份与令牌,并通过IdentityToken消息传递给客户端。这意味着即使客户端不主动申请身份,也能在建立连接后获得一套可复用的凭证——适合快速上手或无需持久化身份的交互式场景。IdentityToken消息类型定义在 crates/client-api-messages/src/websocket/v1.rs 中。
3. 携带第三方 OIDC 令牌
由于身份是从 JWT 的sub/iss声明确定性派生的,客户端也可以直接使用任意 OpenID Connect 兼容身份提供商(IdP)签发的 JWT作为访问凭证,无需事先在 SpacetimeDB 中注册。服务端会通过 OIDC 发现机制(见下文"OIDC 验证器")校验令牌签名后,按iss+sub计算身份。
Authorization请求头规范
SpacetimeDB 的众多 HTTP 端点要么要求、要么可选地接受Authorization头中的令牌。其格式统一为:
Authorization: Bearer ${token}其中token是一个 OpenID Connect 兼容的 JWT,例如 POST /v1/identity 端点返回的令牌。
源码中的凭证提取逻辑
在 crates/client-api/src/auth.rs 中,SpacetimeCreds::from_request_parts展示了凭证的完整提取逻辑——优先从Authorization头读取 Bearer 令牌,其次回退到 URL 查询参数?token=...:
/// Extract credentials from the headers or else query string of a request. fn from_request_parts(parts: &request::Parts) -> Result<Option<Self>, headers::Error> { let header = parts .headers .typed_try_get::<headers::Authorization<authorization::Bearer>>()?; if let Some(headers::Authorization(bearer)) = header { let token = bearer.token().to_owned(); return Ok(Some(SpacetimeCreds { token })); } if let Ok(Query(creds)) = Query::<Self>::try_from_uri(&parts.uri) { return Ok(Some(creds)); } Ok(None) }也就是说,除了标准的Authorization: Bearer <token>头之外,开发者还可以通过?token=<token>查询参数携带凭证(前者优先级更高)。这也是 crates/client-api/src/routes/database.rs 中诸多/v1/database路由所依赖的统一鉴权入口。
请求失败时的标准错误响应
当令牌无效或缺失时,服务端通过AuthorizationRejection类型返回明确的 HTTP 状态码与错误消息(见 crates/client-api/src/auth.rs):
| 场景 | 状态码 | 响应体 |
|---|---|---|
| 令牌有效,但签名不是本集群签发(如密钥已轮换) | 401 Unauthorized | Authorization failed: token not signed by this instance |
| JWT 格式非法或请求头解析失败 | 400 Bad Request | Authorization is invalid: malformed token |
需要认证但未提供Authorization头 | 401 Unauthorized | Authorization required |
| 其他自定义校验失败 | 401 Unauthorized | 具体错误消息 |
匿名访问:默认被允许,但存在权限边界
授权文档明确写道:所有/v1/database端点都支持匿名访问。如果请求未携带Authorization头,SpacetimeDB 会为请求分配一个新的匿名身份。
在源码层面,这一行为由 crates/client-api/src/auth.rs 的get_or_create实现:当请求中没有 JWT 时,直接调用SpacetimeAuth::alloc创建一个全新的身份与令牌,并在响应头中通过spacetime-identity与spacetime-identity-token两个自定义响应头回传给客户端(见SpacetimeIdentity与SpacetimeIdentityToken的类型定义,crates/client-api/src/auth.rs)。
匿名请求可以做什么
- 访问公共信息:数据库信息(database info)、schema、名称(names);
- 调用 reducer 或运行 SQL 查询;
- 访问公共表(public tables)中的数据。
例如 GET /v1/database/:name_or_identity/schema 端点不需要任何授权即可获取 schema;POST /v1/database/:name_or_identity/sql 允许匿名运行 SQL,但只能访问公共表,且调用者身份会被用于强制执行行级安全策略(row-level security)。
匿名请求会被拒绝的操作
- 删除数据库(
DELETE /v1/database/:name_or_identity):删除要求所有权,匿名请求会被拒绝; - 查看日志(
GET /v1/database/:name_or_identity/logs):查看日志要求数据库所有权,匿名请求会被拒绝; - 更新数据库(
PUT /v1/database/:name_or_identity):更新现有数据库时,令牌必须对应数据库的所有者,否则请求被拒绝并返回401 UNAUTHORIZED及{ "PermissionDenied": { "name": string } }形式的 JSON; - 设置名称(
PUT /v1/database/:name_or_identity/names):设置名称列表要求数据库所有权; - 发布新数据库(
POST /v1/database):虽然匿名也可以发布,但新数据库会被这个自动分配的匿名身份所拥有——这"通常不是你想要的"(官方文档原话),因为一旦丢失令牌你将失去对该数据库的管理权。
匿名调用 reducer 的身份传递
通过 POST /v1/database/:name_or_identity/call/:reducer 匿名调用 reducer 时,调用者的身份会通过ReducerContext传递给模块代码,模块可以基于该身份决定接受或拒绝调用。这是模块侧实现自定义授权逻辑的基础——匿名并不等于无身份,只是身份是"每次请求临时分配的"。
令牌验证链路:两级验证与 OIDC 发现
理解"令牌不可移植到其他集群"的关键,在于 SpacetimeDB 的令牌验证实现。核心代码位于 crates/core/src/auth/token_validation.rs:
// This validator accepts any tokens signed with the local key (regardless of issuer). // If it is not signed with the local key, we will try to validate it with the OIDC validator. pub struct FullTokenValidator<T: TokenValidator + Send + Sync> { pub local_key: DecodingKey, pub local_issuer: Box<str>, pub oidc_validator: T, }FullTokenValidator(默认验证器)的验证顺序是:
- 先用本地公钥验证:如果令牌能通过本集群公钥的签名校验(此时不强制校验 issuer,因为 SpacetimeDB 会用自己的密钥为短生命周期令牌重签名),则直接接受;
- 本地验证失败后,尝试 OIDC 验证:从令牌中提取原始
iss声明(该提取过程刻意不做签名校验,仅用于密钥发现),如果iss就是本集群的local_issuer,则返回第一步的错误;否则交给 OIDC 验证器处理。
OIDC 验证器与 JWKS 缓存
OIDC 验证器通过标准的OpenID Connect Discovery流程验证第三方令牌(crates/core/src/auth/token_validation.rs):
- 访问
{issuer}/.well-known/openid-configuration获取jwks_uri; - 访问
jwks_uri拉取 JWKS(JSON Web Key Set); - 根据 JWT 头部的
kid(或遍历全部密钥)找到对应公钥并校验签名与 issuer。
其中CachingOidcTokenValidator会对 JWKS 做缓存:默认每 300 秒刷新一次、缓存有效期 7200 秒(crates/core/src/auth/token_validation.rs),避免每个请求都触发外部 HTTP 调用。同时,validate_url_scheme会强制要求 OIDC URL 仅支持http://或https://协议。
Claim 级校验规则
无论走哪条验证路径,令牌最终都要被转换为SpacetimeIdentityClaims。转换前的严格校验逻辑位于 crates/auth/src/identity.rs,包括:
iss与sub均不得超过 128 字节,且不得为空;- 若令牌中携带了
hex_identity声明,则其值必须与Identity::from_claims(iss, sub)计算出的身份完全一致,否则拒绝; - 过期校验:
exp若存在,则过期超过 60 秒宽限(leeway)的令牌会被拒绝;exp为null或缺失时按"永不过期"处理(兼容旧版令牌)。
SpacetimeIdentityClaims的完整字段定义(含hex_identity、sub、iss、aud、iat、exp及自定义extra声明)见 crates/auth/src/identity.rs。令牌统一使用ES256(ECDSA P-256)算法签名,见 crates/client-api/src/auth.rs。
短生命周期令牌的重签名
POST /v1/identity/websocket-token 端点接收一个有效令牌,并返回一个过期时间为 60 秒的短生命周期令牌,适合嵌入 URL 等不可信场景。其实现create_websocket_token(crates/client-api/src/routes/identity.rs)调用re_sign_with_expiry用本集群密钥重新签名——即使原令牌的 issuer 不同,重签后也会带上 60 秒的exp,且由于FullTokenValidator先验证本集群签名,因此这些重签令牌仍可通过验证。
顶层路由与健康检查:GET /v1/ping
授权文档列出的顶层路由只有一个:
| 路由 | 说明 |
|---|---|
GET /v1/ping | 无操作(No-op),用于判断客户端能否连接到 SpacetimeDB |
GET /v1/ping不执行任何逻辑、也不返回任何数据,客户端可以发送请求到该端点以探测与 SpacetimeDB 主机的连通性。它是"能否连上"的最小可用性探针,不涉及任何身份验证。
配套身份端点全景:把授权能力串起来
授权文档中引用了 POST /v1/identity 作为令牌来源。为便于把授权链路串成一个完整闭环,这里列出/v1/identity下的全部端点(路由注册见 crates/client-api/src/routes/identity.rs):
| 路由 | 说明 | 认证要求 |
|---|---|---|
POST /v1/identity | 生成新的身份与令牌 | 无 |
POST /v1/identity/websocket-token | 生成 60 秒短生命周期令牌 | 需要Authorization(Bearer) |
GET /v1/identity/public-key | 获取本集群用于验证令牌的公钥,Content-Type 为application/pem-certificate-chain | 无 |
GET /v1/identity/:identity/databases | 列出某身份拥有的数据库(返回identities数组) | 无 |
GET /v1/identity/:identity/verify | 验证身份/令牌对 | 需要Authorization(Bearer) |
其中GET /v1/identity/:identity/verify的三种响应语义(crates/client-api/src/routes/identity.rs)非常清晰:
- 令牌有效且与路径中的
:identity匹配 →204 No Content; - 令牌有效但与
:identity不匹配 →400 Bad Request; - 令牌无效或缺失
Authorization头 →401 Unauthorized。
该端点可配合公钥端点(public-key)在不依赖任何 IdP 的情况下自行实现客户端侧的令牌自校验。
实战演练:用 curl 跑通完整授权流程
以下操作基于本地运行的 SpacetimeDB 主机(localhost:3000),演示从申请身份到访问受控资源的完整流程。
1. 探测连通性
curl -i http://localhost:3000/v1/ping预期返回200状态码且无响应体。若无法连接,请先检查主机是否已启动。
2. 申请身份与令牌
curl -s http://localhost:3000/v1/identity返回示例:
{ "identity": "0x5f3c...(64 位十六进制身份)", "token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..." }将token值保存到环境变量供后续使用:
export ST_TOKEN="<上一步返回的 token>"3. 带令牌调用数据库端点
# 携带令牌查询数据库信息 curl -s -H "Authorization: Bearer $ST_TOKEN" \ http://localhost:3000/v1/database/your_db_name # 匿名调用(不带 Authorization 头)——会被分配临时匿名身份 curl -s http://localhost:3000/v1/database/your_db_name对于 schema 类端点,携带令牌时响应头还会回显spacetime-identity与spacetime-identity-token两个响应头。
4. 生成短生命周期令牌(需先持有有效令牌)
curl -s -X POST -H "Authorization: Bearer $ST_TOKEN" \ http://localhost:3000/v1/identity/websocket-token返回:
{ "token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..." }该令牌 60 秒后过期,可用于 URL 内嵌等不可信上下文。
5. 验证身份与令牌对
curl -i -H "Authorization: Bearer $ST_TOKEN" \ http://localhost:3000/v1/identity/<你的 identity>/verify204:令牌有效且身份匹配;400:令牌有效但身份不匹配;401:令牌无效或缺失。
安全实践与注意事项
结合官方文档与源码实现,使用 SpacetimeDB HTTP API 授权时建议注意以下几点:
- 匿名发布数据库需谨慎:
POST /v1/database不带Authorization头时,新数据库归临时匿名身份所有,令牌一旦丢失将无法找回所有权。务必先通过/v1/identity申请身份再用其发布数据库。 - 令牌具备集群边界:自签令牌由本集群私钥签发,不能跨集群使用;更换主机或密钥轮换后,旧令牌会以
401 Authorization failed: token not signed by this instance被拒绝。 - 善用短生命周期令牌:需要把凭证暴露给不可信环境(如 URL、日志、第三方)时,优先使用
POST /v1/identity/websocket-token生成的 60 秒令牌,避免泄露长期令牌。 - 模块内仍需自行鉴权:HTTP 层的匿名访问是"允许访问公共资源",但模块内的 reducer 需要基于
ReducerContext中的调用者身份自行实现业务级授权,并善用表的公共/私有(table_access)声明与行级安全策略。 - 了解验证器的 OIDC 依赖:使用第三方 OIDC 令牌时,主机需要能够访问
{issuer}/.well-known/openid-configuration与jwks_uri;JWKS 默认缓存 5 分钟、最多 2 小时,IdP 密钥轮换后存在短暂窗口期。
延伸阅读
- 授权文档原文:HTTP API 授权
- 身份端点全解:HTTP API 身份端点
- 数据库端点与各端点授权要求:HTTP API 数据库端点
- 身份派生实现:crates/lib/src/identity.rs
- 令牌签发与匿名中间件:crates/client-api/src/auth.rs
- 身份端点路由实现:crates/client-api/src/routes/identity.rs
- 令牌验证链路:crates/core/src/auth/token_validation.rs
- Claim 结构与校验规则:crates/auth/src/identity.rs
【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考