资讯动态

SpacetimeDB Auth Claims 使用指南:在 Reducer 中读取与校验 JWT 声明

发布时间:2026/9/13 4:47:36 来源:尧图企业网站定制
SpacetimeDB Auth Claims 使用指南在 Reducer 中读取与校验 JWT 声明【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB本指南以 SpacetimeDB 官方文档 Using Auth Claims 为主体系统讲解如何在 Reducer 中通过ReducerContext访问客户端 JWT 携带的认证声明auth claims覆盖常见声明读取、签发方issuer与受众audience校验、自定义声明解析等实战场景并提供 TypeScript、C#、Rust、C 四种服务端语言的完整实现帮助你在模块开发中快速落地基于 OIDC 的身份鉴权。SpacetimeDB 允许你在模块的 reducer 中轻松访问内嵌于 OIDC 兼容 JWT 令牌中的认证声明。Auth claims 是键值对用于描述已认证用户的相关信息例如用户的唯一 ID、邮箱或认证提供方。如果你想查看这些字段的实际内容可以使用 jwt.io 之类的在线工具解析任意 JWT。在 SpacetimeDB 的 reducer 中你可以通过ReducerContext从客户端令牌访问这些 auth claims。下文将展示如何使用这些声明。理解 SpacetimeDB 中的 Auth Claims 与 ReducerContext在深入代码之前先明确两个核心概念在源码中的落点ReducerContext是每个 reducer 的入口参数其sender_auth()方法Rust/senderAuth属性TypeScript/SenderAuth属性C#返回当前调用者的认证上下文AuthCtx。从源码看Rust 侧的AuthCtx结构位于 crates/bindings/src/lib.rs包含两个关键字段is_internal是否为数据库内部调用与jwt懒加载的 JWT 声明。JwtClaims是对 JWT 载荷的封装crates/bindings/src/lib.rs内部持有完整的原始载荷字符串并通过OnceCell惰性解析出subject()、issuer()、audience()、identity()与raw_payload()。值得注意的一个安全细节AuthCtx::internal()构造的上下文没有任何 JWT代表定时调度scheduledreducer 发起的内部调用而普通客户端连接会通过rt::get_jwt(connection_id)从连接中懒加载令牌见 crates/bindings/src/lib.rs。这也解释了为什么从客户端连接时的 token 读取 claims与内部调度任务无 token两种场景需要区别处理。访问常见声明Subject 与 IssuerJWT 中最常被访问的两个声明是 subjectsub和 issueriss。Issuer 表明令牌由哪个认证提供方签发subject 是签发方分配给用户的唯一标识符。二者都是必填声明SpacetimeDB 正是基于它们计算每个用户的Identity源码中JwtClaims::identity()即调用Identity::from_claims(issuer, subject)见 crates/bindings/src/lib.rs。由于使用频率极高SpacetimeDB 为它们提供了便捷的辅助函数。TypeScriptimport { SenderError } from spacetimedb/server; export const onConnect spacetimedb.clientConnected(ctx { const jwt ctx.senderAuth.jwt; if (jwt null) { throw new SenderError(Unauthorized: JWT is required to connect); } console.info(Client connected with sub: ${jwt.subject}, iss: ${jwt.issuer}); });TS 侧JwtClaims的类型定义位于 crates/bindings-typescript/src/lib/reducers.ts除了subject、issuer、audience外还直接暴露identity与fullPayload整个载荷作为 JSON 对象后文的自定义声明解析会用到它。C#[Reducer(ReducerKind.ClientConnected)] public static void ClientConnected(ReducerContext ctx) { var claims ctx.SenderAuth.Jwt ?? throw new Exception(Client connected without JWT); Log.Info($Client connected with csub: {claims.Subject}, and iss: {claims.Issuer}); }C# 侧JwtClaims的实现位于 crates/bindings-csharp/Runtime/JwtClaims.cs。注意其Subject、Issuer属性在缺失或类型不正确时会抛出InvalidOperationException与 Rust 侧的expect/panic行为类似——这提醒我们读取声明前应先确认 JWT 存在。Rust#[reducer(client_connected)] pub fn connect(ctx: ReducerContext) - Result(), String { let auth_ctx ctx.sender_auth(); let (subject, issuer) match auth_ctx.jwt() { Some(claims) (claims.subject().to_string(), claims.issuer().to_string()), None { return Err(Client connected without JWT.to_string()); } }; log::info!(sub: {}, iss: {}, subject, issuer); Ok(()) }使用 Google 签发的令牌时示例输出如下INFO: src\lib.rs:64: sub: 321321321321321, iss: https://accounts.google.comCSPACETIMEDB_CLIENT_CONNECTED(auth_claims_connect, ReducerContext ctx) { const auto auth ctx.sender_auth(); const auto jwt_opt auth.get_jwt(); if (!jwt_opt.has_value()) { return Err(Client connected without JWT); } const JwtClaims jwt *jwt_opt; const std::string subject jwt.subject(); const std::string issuer jwt.issuer(); LOG_INFO(sub: subject , iss: issuer); return Ok(); }C 侧的AuthCtx/JwtClaims定义在 crates/bindings-cpp/include/spacetimedb/auth_ctx.h。从实现可以看到AuthCtx通过std::functionstd::optionalJwtClaims()懒加载器包装令牌get_jwt()在is_internal_为 true 时直接返回空值与 Rust 侧行为保持一致。注C 模块支持依赖 C 模块版本CppModuleVersionNotice /使用前请确认你的 SDK 版本。示例限制认证提供方Issuer 校验由于任何持有合法令牌的用户都可以连接 SpacetimeDB其令牌可能来自任意认证提供方。例如用户可能发送一个由 GitHub 签发的 OIDC 兼容令牌而你只想接受 Google 签发的令牌。因此通常有必要将模块访问限制为你自己的 issuer 或特定 issuer。最佳实践是客户端连接时至少校验 issuer确保你的数据只能被你应用的用户访问。此外还必须校验audaudience声明确保签发方确实打算把令牌交给你的应用使用。这能防止其他应用将原本签发给别的应用的令牌转发给你、造成令牌滥用。例如我们可以将访问限制为使用 SpacetimeAuth 凭据的客户端。TypeScriptconst OIDC_CLIENT_IDS [client_XXXXXXXXXXXXXXXXXXXXXX]; export const onConnect spacetimedb.clientConnected(ctx { const jwt ctx.senderAuth.jwt; if (jwt null) { throw new SenderError(Unauthorized: JWT is required to connect); } if (jwt.issuer ! https://auth.spacetimedb.com/oidc) { throw new SenderError(Unauthorized: Invalid issuer ${jwt.issuer}); } if (!jwt.audience.some(aud OIDC_CLIENT_IDS.includes(aud))) { throw new SenderError(Unauthorized: Invalid audience ${jwt.audience}); } });C#// The oidc client ids configured for SpacetimeAuth. public static readonly Liststring OIDC_CLIENT_IDS new() { client_XXXXXXXXXXXXXXXXXXXXXX, }; public void Connect(ReducerContext ctx) { var claims ctx.SenderAuth.Jwt ?? throw new Exception(Client connected without JWT); if (claims.Issuer ! https://auth.spacetimedb.com/oidc) { throw new Exception(Unauthorized: invalid issuer); } if (!OIDC_CLIENT_IDS.Any(s claims.Audience.Contains(s))) { throw new Exception(Unauthorized: invalid audience); } }Rust// Set this to your the OIDC client (or set of clients) set up for your // SpacetimeAuth project. const OIDC_CLIENT_ID: str client_XXXXXXXXXXXXXXXXXXXXXX; #[reducer(client_connected)] pub fn connect(ctx: ReducerContext) - Result(), String { let jwt ctx.sender_auth().jwt().ok_or(Authentication required.to_string())?; if jwt.issuer() ! https://auth.spacetimedb.com/oidc { return Err(Invalid issuer.to_string()); } if !jwt.audience().iter().any(|a| a OIDC_CLIENT_ID) { return Err(Invalid audience.to_string()); } Ok(()) }Cconst std::string SPACETIME_OIDC_ISSUER https://auth.spacetimedb.com/oidc; const std::string SPACETIME_OIDC_CLIENT_ID client_XXXXXXXXXXXXXXXXXXXXXX; SPACETIMEDB_CLIENT_CONNECTED(restrict_auth_provider_connect, ReducerContext ctx) { const auto auth ctx.sender_auth(); const auto jwt_opt auth.get_jwt(); if (!jwt_opt.has_value()) { return Err(Authentication required); } const JwtClaims jwt *jwt_opt; if (jwt.issuer() ! SPACETIME_OIDC_ISSUER) { return Err(Invalid issuer); } const auto audience jwt.audience(); bool found false; for (const auto aud : audience) { if (aud SPACETIME_OIDC_CLIENT_ID) { found true; break; } } if (!found) { return Err(Invalid audience); } return Ok(); }从源码实现看aud声明在解析时既支持字符串也支持字符串数组Rust 侧extract_audience()crates/bindings/src/lib.rs遇到字符串类型返回单元素数组遇到数组类型逐项过滤出字符串C# 侧ExtractAudience()crates/bindings-csharp/Runtime/JwtClaims.cs逻辑一致遇到其他类型则抛出异常。因此上面各语言中遍历 audience 列表检查是否包含目标 client id的写法都是安全的。若令牌缺少aud声明Rust 与 C# 实现都会返回空列表而非报错校验逻辑会据此拒绝连接行为符合预期。访问自定义声明Custom Claims如果你想访问辅助函数未覆盖的额外声明可以解析完整 JWT 载荷。这对于处理自定义的或应用特定的声明非常有用。举个例子假设你的令牌带有一个roles声明它是权限列表。如果你希望只有拥有admin角色的用户才能调用某个 reducer可以这样做。TypeScriptimport { SenderError, type InferSchema, type ReducerCtx } from spacetimedb/server; type Ctx ReducerCtxInferSchematypeof spacetimedb; // Return an error to the client if they dont have admin rights. function ensureAdminAccess(ctx: Ctx) { const auth ctx.senderAuth; if (auth.isInternal) { return; } const jwt auth.jwt; if (jwt null) { throw new SenderError(Unauthorized: JWT is required); } const roles jwt.fullPayload[roles]; if (!Array.isArray(roles) || !roles.includes(admin)) { throw new SenderError(Unauthorized: Admin role is required); } } export const adminonly spacetimedb.reducer(ctx { ensureAdminAccess(ctx); });TS 侧无需额外依赖JwtClaims.fullPayload已经是一个JsonObject见 crates/bindings-typescript/src/lib/reducers.ts可以直接按属性名取自定义字段。C#// Throw if the sender does not have admin access. private static void EnsureAdminAccess(ReducerContext ctx) { var auth ctx.SenderAuth; if (auth.IsInternal) { return; } var claims auth.Jwt ?? throw new Exception(Missing JWT claims); using var jwtPayload JsonDocument.Parse(claims.RawPayload); var root jwtPayload.RootElement; bool hasAdmin root.TryGetProperty(roles, out var rolesProp) rolesProp.ValueKind JsonValueKind.Array rolesProp.EnumerateArray().Any(e e.ValueKind JsonValueKind.String e.GetString() admin ); if (!hasAdmin) { throw new Exception(Unauthorized: admin role required); } } [Reducer] public static void AdminOnlyReducer(ReducerContext ctx) { EnsureAdminAccess(ctx); // We can now be sure that the caller is an admin. }C# 侧使用System.Text.Json的JsonDocument.Parse解析JwtClaims.RawPayload即原始载荷字符串见 crates/bindings-csharp/Runtime/JwtClaims.cs。Rust更新Cargo.toml添加serde与serde_json用于解析 JSON[dependencies] ... serde { version 1.0.219, features [derive] } serde_json 1.0.143#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct CustomClaims { pub roles: VecString, } /// Returns Ok(()) if the sender has admin access, Err otherwise. fn ensure_admin_access(sender_auth: spacetimedb::AuthCtx) - Result(), String { if sender_auth.is_internal() { // This is a scheduled reducer, so it should already be trusted. return Ok(()); } let jwt sender_auth.jwt().ok_or(Authentication required.to_string())?; let claims: CustomClaims serde_json::from_slice(jwt.raw_payload().as_bytes()).map_err(|e| format!(Client connected with invalid JWT: {}, e).to_string())?; if claims.roles.iter().any(|r| r admin) { return Ok(()); } Err(Admin role required.to_string()) } #[spacetimedb::reducer] pub fn admin_only_reducer(ctx: ReducerContext) - Result(), String { ensure_admin_access(ctx.sender_auth())?; // Now we can safely perform admin-only actions. Ok(()) }Rust 侧raw_payload()crates/bindings/src/lib.rs返回完整的 JWT 载荷 JSON 字符串配合serde_json::from_slice即可反序列化成你自定义的CustomClaims结构体。C// For robust JSON parsing, this example uses nlohmann/json (header-only). bool ensure_admin_access(const AuthCtx auth) { if (auth.is_internal()) { // Scheduled reducers are trusted return true; } const auto jwt_opt auth.get_jwt(); if (!jwt_opt.has_value()) { return false; } const auto payload jwt_opt-raw_payload(); auto json nlohmann::json::parse(payload, nullptr, false); if (json.is_discarded()) { return false; } if (!json.contains(roles) || !json[roles].is_array()) { return false; } for (const auto role : json[roles]) { if (role.is_string() role.getstd::string() admin) { return true; } } return false; } SPACETIMEDB_REDUCER(admin_only_reducer, ReducerContext ctx) { if (!ensure_admin_access(ctx.sender_auth())) { return Err(Admin role required); } // We can now safely perform admin-only actions. LOG_INFO(Admin action performed); return Ok(); }C 示例采用 header-only 的 nlohmann/json 库进行稳健的 JSON 解析。注意其中使用了nlohmann::json::parse(payload, nullptr, false)——第三个参数false表示解析失败时不抛异常而是标记is_discarded()随后显式检查避免畸形载荷导致崩溃。一个容易被忽略的场景内部调用Scheduled Reducer上面四个自定义声明示例中都先判断了is_internal/IsInternal定时调度的 reducer 由数据库内部发起AuthCtx中不存在 JWT。若不加判断直接要求 JWT内部任务会全部失败而如果无条件信任带 JWT 的调用又可能放过外部伪造请求。正确的模式是内部调用直接放行信任调度器外部调用严格校验 JWT 声明。这一点在 Rust 实现crates/bindings/src/lib.rs 的AuthCtx::internal()与各语言 SDK 中均已内建支持。最佳实践与安全要点始终校验 JWT 声明的存在性与内容再将其用于应用逻辑。读取sub/iss前先检查jwt()/Jwt/get_jwt()是否为空缺失令牌时直接拒绝请求。自定义业务逻辑中反序列化 JWT 载荷以访问默认未解析的额外声明如上述roles示例。在合适的场景限制接受的 issuer以执行安全策略同时校验aud防止其他应用转用你的令牌。区分内部调用与外部调用定时调度 reducer 无 JWT应通过is_internal()放行而非误报为未授权。若你需要进一步了解 SpacetimeDB 认证体系的全貌可继续阅读同目录下的 SpacetimeAuth 使用文档 及 Auth0、Clerk、BetterAuth 等集成指南。【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价