ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpacetimeDB 表访问权限完全指南:公开/私有表与基于 View 的细粒度数据管控

SpacetimeDB 表访问权限完全指南:公开/私有表与基于 View 的细粒度数据管控 SpacetimeDB 表访问权限完全指南公开/私有表与基于 View 的细粒度数据管控【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDBSpacetimeDB 通过表可见性 执行上下文两层机制控制数据访问表分为公开public与私有private两类而 reducer、procedure、view 与客户端各自拥有不同的数据读写能力。本文完整讲解表访问权限模型并结合仓库源码演示如何利用公开/私有表界定边界、如何用只读 View 按调用者过滤行、隐藏敏感列构建一套行级与列级的细粒度访问控制方案。权限模型总览谁可以访问什么在深入细节之前先用一张表概括 SpacetimeDB 的访问控制全景依据官方文档整理访问主体可访问范围读写能力说明Reducer全部表公开 私有读 写增删改查运行在服务端持有ReducerContextProcedure全部表公开 私有读 写必须用withTx显式开启事务View全部表公开 私有只读查询、迭代持有ViewContext/AnonymousViewContext仅限索引查找客户端仅公开表与公开 View只读订阅、查询修改数据只能通过调用 reducer / procedure关键结论私有表对客户端完全不可见不能查询、不能订阅公开表对客户端只开放读任何写操作都必须经由服务端函数完成。公开表与私有表数据可见性的第一道边界默认私有显式公开SpacetimeDB 中表默认是私有的private。私有表只能由运行在服务端的 reducer 和 view 访问客户端既无法查询、也无法订阅、更看不到私有表中的任何数据。公开表public则通过订阅subscription和查询query向客户端开放读访问但客户端依然只能通过调用 reducer 来修改公开表的数据不能直接写入。以下分别用 TypeScript、C#、Rust 定义私有表与公开表分别来自原文档对应语言的完整示例// Private table (default) - only accessible from server-side code const internalConfig table( { name: internal_config }, { key: t.string().primaryKey(), value: t.string(), } ); // Public table - clients can subscribe and query const player table( { name: player, public: true }, { id: t.u64().primaryKey().autoInc(), name: t.string(), score: t.u64(), } );// Private table (default) - only accessible from server-side code [SpacetimeDB.Table(Name InternalConfig)] public partial struct InternalConfig { [SpacetimeDB.PrimaryKey] public string Key; public string Value; } // Public table - clients can subscribe and query [SpacetimeDB.Table(Name Player, Public true)] public partial struct Player { [SpacetimeDB.PrimaryKey] [SpacetimeDB.AutoInc] public ulong Id; public string Name; public ulong Score; }// Private table (default) - only accessible from server-side code #[spacetimedb::table(name internal_config)] pub struct InternalConfig { #[primary_key] key: String, value: String, } // Public table - clients can subscribe and query #[spacetimedb::table(name player, public)] pub struct Player { #[primary_key] #[auto_inc] id: u64, name: String, score: u64, }注意 Rust 与 TypeScript 的差异Rust 中通过#[spacetimedb::table(name player, public)]属性内的public关键字声明TypeScript 通过table({ name: player, public: true }, ...)配置C# 通过[SpacetimeDB.Table(Name Player, Public true)]的Public true命名参数声明。可见性的源码实现表可见性并非文档层面的虚构概念而是被完整落地在模块宏解析与数据存储层宏解析层Rust 端#[spacetimedb::table]宏在 crates/bindings-macro/src/table.rs 中解析sym::public元数据C# 端对应Public命名参数。系统表记录st_tables系统表schema 定义于 crates/datastore/src/system_tables.rs保存每个表的is_public布尔列st_views系统表同样为每个 view 记录is_public与is_anonymous见下文。订阅/查询过滤客户端订阅 SQL 时引擎会依据该标志决定哪些表可以下发到客户端。如何选择使用私有表的场景内部配置或客户端不应看到的状态敏感数据如密码哈希password hash、API 密钥API key中间计算结果。使用公开表的场景客户端需要展示或交互的数据游戏状态、用户资料等面向用户的数据。Reducer拥有完整读写权限Reducer 接收ReducerContext对**所有表包括公开表和私有表**拥有完整读写权限可执行全部 CRUD 操作插入insert、读取read、更新update、删除delete。三种语言的完整示例spacetimedb.reducer(example, {}, (ctx) { // Insert ctx.db.user.insert({ id: 0, name: Alice, email: aliceexample.com }); // Read: iterate all rows for (const user of ctx.db.user.iter()) { console.log(user.name); } // Read: find by unique column const foundUser ctx.db.user.id.find(123); if (foundUser) { // Update foundUser.name Bob; ctx.db.user.id.update(foundUser); } // Delete ctx.db.user.id.delete(456); });[SpacetimeDB.Reducer] public static void Example(ReducerContext ctx) { // Insert ctx.Db.User.Insert(new User { Id 0, Name Alice, Email aliceexample.com }); // Read: iterate all rows foreach (var user in ctx.Db.User.Iter()) { Log.Info($User: {user.Name}); } // Read: find by unique column if (ctx.Db.User.Id.Find(123) is User foundUser) { // Update foundUser.Name Bob; ctx.Db.User.Id.Update(foundUser); } // Delete ctx.Db.User.Id.Delete(456); }#[spacetimedb::reducer] fn example(ctx: ReducerContext) - Result(), String { // Insert ctx.db.user().insert(User { id: 0, name: Alice.to_string(), email: aliceexample.com.to_string(), }); // Read: iterate all rows for user in ctx.db.user().iter() { log::info!(User: {}, user.name); } // Read: find by unique column if let Some(mut user) ctx.db.user().id().find(123) { // Update user.name Bob.to_string(); ctx.db.user().id().update(user); } // Delete ctx.db.user().id().delete(456); Ok(()) }可见同一套 CRUD 惯例在三种语言 SDK 中保持一致iter()遍历、index.find()按唯一列查找、修改行字段后通过index.update()写回、index.delete()按键删除。Procedure通过显式事务获得读写权限Procedure 接收ProcedureContext同样可以访问所有表但它与 reducer 的关键区别是procedure 不会自动运行在数据库事务中必须显式打开事务withTx/WithTx/with_tx才能读取或修改数据库。spacetimedb.procedure(updateUserProcedure, { userId: t.u64(), newName: t.string() }, t.unit(), (ctx, { userId, newName }) { // Must explicitly open a transaction ctx.withTx(ctx { // Full read-write access within the transaction const user ctx.db.user.id.find(userId); if (user) { user.name newName; ctx.db.user.id.update(user); } }); // Transaction is committed when the function returns return {}; });#pragma warning disable STDB_UNSTABLE [SpacetimeDB.Procedure] public static void UpdateUserProcedure(ProcedureContext ctx, ulong userId, string newName) { // Must explicitly open a transaction ctx.WithTx(txCtx { // Full read-write access within the transaction var user txCtx.Db.User.Id.Find(userId); if (user ! null) { var updated user.Value; updated.Name newName; txCtx.Db.User.Id.Update(updated); } return 0; }); // Transaction is committed when the lambda returns }#[spacetimedb::procedure] fn update_user_procedure(ctx: mut ProcedureContext, user_id: u64, new_name: String) { // Must explicitly open a transaction ctx.with_tx(|ctx| { // Full read-write access within the transaction if let Some(mut user) ctx.db.user().id().find(user_id) { user.name new_name.clone(); ctx.db.user().id().update(user); } }); // Transaction is committed when the closure returns }事务的提交与回滚语义根据 Procedures 官方文档原文档中../00200-functions/00400-procedures.md的相对链接此处已转换为仓库根路径传入withTx的闭包/函数返回时事务提交对数据库状态的修改永久生效并被广播给客户端闭包抛出错误/异常/panic 时事务回滚所有修改被丢弃Rust 中对于可能失败的事务文档建议优先使用try_with_tx返回Result而不是依赖 panic 回滚。⚠️ 重要withTx闭包可能被多次调用官方文档特别警告传入withTx的函数可能被多次调用且每次可能看到不同版本的数据库状态若基于同一数据库状态被多次调用闭包必须执行相同操作、返回相同结果若基于不同数据库状态被调用先前运行中观察到的值不得影响该函数或调用方 procedure 的行为应避免在withTx闭包中捕获可变状态。这意味着写事务逻辑时要保持纯函数式风格不要在闭包外积累副作用。另外注意Procedure 目前处于 beta / unstable 阶段——Rust 模块需在Cargo.toml中为spacetimedb依赖启用unstablefeatureC# 文件顶部需加#pragma warning disable STDB_UNSTABLE且 API 可能在未来版本中变化。详见 Procedures 文档。View只读访问View 接收ViewContext或AnonymousViewContext对所有表公开 私有提供只读访问可以查询、迭代表但不能插入、更新或删除行。这种只读约束直接体现在类型系统上——三种 SDK 中 View 的 context 都不暴露写操作方法spacetimedb.view( { name: findUsersByName, public: true }, t.array(user.rowType), (ctx) { // Can read and filter return Array.from(ctx.db.user.name.filter(Alice)); // Cannot insert, update, or delete // ctx.db.user.insert(...) // ❌ Method not available });[SpacetimeDB.View(Name FindUsersByName, Public true)] public static ListUser FindUsersByName(ViewContext ctx) { // Can read and filter return ctx.Db.User.Name.Filter(Alice).ToList(); // Cannot insert, update, or delete // ctx.Db.User.Insert(...) // ❌ Method not available }#[spacetimedb::view(name find_users_by_name, public)] fn find_users_by_name(ctx: ViewContext) - VecUser { // Can read and filter ctx.db.user().name().filter(Alice).collect() // Cannot insert, update, or delete // ctx.db.user().insert(...) // ❌ Compile error }View 必须是 public且仅限索引查找强制公开官方文档规定 View 必须声明为public并带有显式name。这一约束在宏层有强制校验——Rust 端 crates/bindings-macro/src/view.rs 在解析#[spacetimedb::view(...)]时若缺少public元数据会直接报编译错误views must be public, e.g. #[view(public)]。仅限索引查找View 只能通过索引indexed lookup访问表数据不能全表扫描scan。该限制用于保证 View 的性能避免每个订阅者的视图计算退化为全表遍历。系统表佐证st_views系统表的StViewRow见 crates/datastore/src/system_tables.rs记录了is_public与is_anonymous两个字段其中注释明确指出当前仅支持公开视图私有视图可能在将来支持。ViewContext 与 AnonymousViewContext 的选择View 有两种 context选择直接影响性能Context 类型提供的能力适用场景ViewContext通过ctx.sender获取调用者Identity结果依赖查询者身份如我的消息我的背包AnonymousViewContext不提供调用者信息结果与查询者无关如全局排行榜、商店库存、世界地图区域性能差异匿名 View 对所有订阅者是共享的——SpacetimeDB 只需物化一次底层数据变化时重算一次并广播给所有人而使用ViewContext的按用户 View 需要为每个订阅者单独计算和跟踪变更1000 个在线用户就是 1000 份独立计算。因此官方建议能设计成与调用者无关的 View 就优先用AnonymousViewContext例如把我附近的实体改造成X 区域内的实体让同区域玩家共享同一份物化结果。用 View 实现细粒度访问控制表可见性解决客户端能否访问某张表的问题而 View 解决客户端能看到哪些行、哪些列的细粒度问题。View 可以读取私有表然后只把对每个客户端合适的数据暴露出来。技巧一按调用者过滤行行级安全利用ViewContext的ctx.sender调用者身份配合索引查找只返回属于调用者自己的行。经典场景是私信系统所有消息存在同一张私有表中客户端查询公开的my_messages视图时只会看到自己是发送方或接收方的消息。import { table, t, schema } from spacetimedb/server; // Private table containing all messages const message table( { name: message }, // Private by default { id: t.u64().primaryKey().autoInc(), sender: t.identity().index(btree), recipient: t.identity().index(btree), content: t.string(), timestamp: t.timestamp(), } ); const spacetimedb schema(message); // Public view that only returns messages the caller can see spacetimedb.view( { name: my_messages, public: true }, t.array(message.rowType), (ctx) { // Look up messages by index where caller is sender or recipient const sent Array.from(ctx.db.message.sender.filter(ctx.sender)); const received Array.from(ctx.db.message.recipient.filter(ctx.sender)); return [...sent, ...received]; } );using SpacetimeDB; public partial class Module { // Private table containing all messages [SpacetimeDB.Table(Name Message)] // Private by default public partial struct Message { [SpacetimeDB.PrimaryKey] [SpacetimeDB.AutoInc] public ulong Id; [SpacetimeDB.Index.BTree] public Identity Sender; [SpacetimeDB.Index.BTree] public Identity Recipient; public string Content; public Timestamp Timestamp; } // Public view that only returns messages the caller can see [SpacetimeDB.View(Name MyMessages, Public true)] public static ListMessage MyMessages(ViewContext ctx) { // Look up messages by index where caller is sender or recipient var sent ctx.Db.Message.Sender.Filter(ctx.Sender).ToList(); var received ctx.Db.Message.Recipient.Filter(ctx.Sender).ToList(); sent.AddRange(received); return sent; } }use spacetimedb::{Identity, Timestamp, ViewContext}; // Private table containing all messages #[spacetimedb::table(name message)] // Private by default pub struct Message { #[primary_key] #[auto_inc] id: u64, #[index(btree)] sender: Identity, #[index(btree)] recipient: Identity, content: String, timestamp: Timestamp, } // Public view that only returns messages the caller can see #[spacetimedb::view(name my_messages, public)] fn my_messages(ctx: ViewContext) - VecMessage { // Look up messages by index where caller is sender or recipient let sent: Vec_ ctx.db.message().sender().filter(ctx.sender).collect(); let received: Vec_ ctx.db.message().recipient().filter(ctx.sender).collect(); sent.into_iter().chain(received).collect() }虽然所有消息都存储在同一张表中但客户端查询my_messages时只能看到自己的消息。注意这里用到了sender、recipient两列上的 BTree 索引——这正是 View 仅限索引查找约束的直接体现。技巧二隐藏敏感列列级安全View 可以从包含敏感数据的表读取然后返回一个不含敏感列的自定义类型投影只暴露客户端应该看到的列。例如user_account私有表存有密码哈希与 API 密钥公开的my_profile视图只返回id、username、created_atimport {schema, t, table} from spacetimedb/server; // Private table with sensitive data const userAccount table( { name: user_account }, // Private by default { id: t.u64().primaryKey().autoInc(), identity: t.identity().unique(), username: t.string(), email: t.string(), passwordHash: t.string(), // Sensitive apiKey: t.string(), // Sensitive createdAt: t.timestamp(), } ); const spacetimedb schema(userAccount); // Public type without sensitive columns const publicUserProfile t.row(PublicUserProfile, { id: t.u64(), username: t.string(), createdAt: t.timestamp(), }); // Public view that returns the callers profile without sensitive data spacetimedb.view( { name: my_profile, public: true }, t.option(publicUserProfile), (ctx) { // Look up the callers account by their identity (unique index) const user ctx.db.userAccount.identity.find(ctx.sender); if (!user) return null; return { id: user.id, username: user.username, createdAt: user.createdAt, // email, passwordHash, and apiKey are not included }; } );using SpacetimeDB; public partial class Module { // Private table with sensitive data [SpacetimeDB.Table(Name UserAccount)] // Private by default public partial struct UserAccount { [SpacetimeDB.PrimaryKey] [SpacetimeDB.AutoInc] public ulong Id; [SpacetimeDB.Unique] public Identity Identity; public string Username; public string Email; public string PasswordHash; // Sensitive public string ApiKey; // Sensitive public Timestamp CreatedAt; } // Public type without sensitive columns [SpacetimeDB.Type] public partial struct PublicUserProfile { public ulong Id; public string Username; public Timestamp CreatedAt; } // Public view that returns the callers profile without sensitive data [SpacetimeDB.View(Name MyProfile, Public true)] public static PublicUserProfile? MyProfile(ViewContext ctx) { // Look up the callers account by their identity (unique index) if (ctx.Db.UserAccount.Identity.Find(ctx.Sender) is not UserAccount user) { return null; } return new PublicUserProfile { Id user.Id, Username user.Username, CreatedAt user.CreatedAt, // Email, PasswordHash, and ApiKey are not included }; } }use spacetimedb::{SpacetimeType, ViewContext, Timestamp, Identity}; // Private table with sensitive data #[spacetimedb::table(name user_account)] // Private by default pub struct UserAccount { #[primary_key] #[auto_inc] id: u64, #[unique] identity: Identity, username: String, email: String, password_hash: String, // Sensitive api_key: String, // Sensitive created_at: Timestamp, } // Public type without sensitive columns #[derive(SpacetimeType)] pub struct PublicUserProfile { id: u64, username: String, created_at: Timestamp, } // Public view that returns the callers profile without sensitive data #[spacetimedb::view(name my_profile, public)] fn my_profile(ctx: ViewContext) - OptionPublicUserProfile { // Look up the callers account by their identity (unique index) let user ctx.db.user_account().identity().find(ctx.sender)?; Some(PublicUserProfile { id: user.id, username: user.username, created_at: user.created_at, // email, password_hash, and api_key are not included }) }客户端可以查询my_profile看到自己的用户名和创建时间但永远看不到邮箱、密码哈希或 API 密钥。这里identity列使用唯一索引t.identity().unique()/[SpacetimeDB.Unique]/#[unique]使find(ctx.sender)成为 O(1) 的索引查找。技巧三组合行过滤与列投影两种技巧可以自由叠加既按调用者过滤行又对返回结果做列投影。示例返回与调用者同部门的同事列表同时隐藏薪资列。import { table, t, schema } from spacetimedb/server; // Private table with all employee data const employee table( { name: employee }, { id: t.u64().primaryKey(), identity: t.identity().unique(), name: t.string(), department: t.string().index(btree), salary: t.u64(), // Sensitive } ); const spacetimedb schema(employee); // Public type for colleagues (no salary) const colleague t.row(Colleague, { id: t.u64(), name: t.string(), department: t.string(), }); // View that returns colleagues in the callers department, without salary info spacetimedb.view( { name: my_colleagues, public: true }, t.array(colleague), (ctx) { // Find the callers employee record by identity (unique index) const me ctx.db.employee.identity.find(ctx.sender); if (!me) return []; // Look up employees in the same department return Array.from(ctx.db.employee.department.filter(me.department)).map(emp ({ id: emp.id, name: emp.name, department: emp.department, // salary is not included })); } );using SpacetimeDB; public partial class Module { // Private table with all employee data [SpacetimeDB.Table(Name Employee)] public partial struct Employee { [SpacetimeDB.PrimaryKey] public ulong Id; [SpacetimeDB.Unique] public Identity Identity; public string Name; [SpacetimeDB.Index.BTree] public string Department; public ulong Salary; // Sensitive } // Public type for colleagues (no salary) [SpacetimeDB.Type] public partial struct Colleague { public ulong Id; public string Name; public string Department; } // View that returns colleagues in the callers department, without salary info [SpacetimeDB.View(Name MyColleagues, Public true)] public static ListColleague MyColleagues(ViewContext ctx) { // Find the callers employee record by identity (unique index) if (ctx.Db.Employee.Identity.Find(ctx.Sender) is not Employee me) { return new ListColleague(); } // Look up employees in the same department return ctx.Db.Employee.Department.Filter(me.Department) .Select(emp new Colleague { Id emp.Id, Name emp.Name, Department emp.Department, // Salary is not included }) .ToList(); } }use spacetimedb::{SpacetimeType, Identity, ViewContext}; // Private table with all employee data #[spacetimedb::table(name employee)] pub struct Employee { #[primary_key] id: u64, #[unique] identity: Identity, name: String, #[index(btree)] department: String, salary: u64, // Sensitive } // Public type for colleagues (no salary) #[derive(SpacetimeType)] pub struct Colleague { id: u64, name: String, department: String, } // View that returns colleagues in the callers department, without salary info #[spacetimedb::view(name my_colleagues, public)] fn my_colleagues(ctx: ViewContext) - VecColleague { // Find the callers employee record by identity (unique index) let Some(me) ctx.db.employee().identity().find(ctx.sender) else { return vec![]; }; // Look up employees in the same department ctx.db.employee().department().filter(me.department) .map(|emp| Colleague { id: emp.id, name: emp.name.clone(), department: emp.department.clone(), // salary is not included }) .collect() }整个查询链路完全基于索引先通过identity唯一索引定位调用者自己的员工记录再通过department的 BTree 索引筛出同部门同事——两步都是索引查找符合 View 的性能约束。客户端访问只读 订阅客户端连接数据库后只能访问公开表与公开 View访问方式有两种订阅Subscriptions客户端通过 SQL 订阅查询SpacetimeDB 立即下发匹配的行之后每当这些行发生变化就推送增量更新。完整的客户端订阅工作流参见 Subscriptions 文档连接建立后通过subscriptionBuilder().subscribe([...])发起订阅onApplied回调触发后即可从本地缓存读取初始数据并通过onInsert/onDelete/onUpdate监听行变化。查询Queries直接查询公开表与公开 View 的结果。客户端不能直接访问私有表对数据的任何修改都必须通过调用 reducer或 procedure来完成。这正是表可见性与执行上下文权限两层模型在客户端侧的最终体现。权限模型在引擎层的落点从源码层面看这套权限模型在引擎与数据存储层有明确实现模块更新与迁移当新模块定义改变了表的可见性时crates/engine/src/update.rs 中的ChangeTableAccess迁移步骤会根据table_def.table_access或view_def.is_public计算新的TableAccess并调用alter_table_access更新数据存储。系统表元数据crates/datastore/src/system_tables.rs 中StViewRow的is_public与is_anonymous字段持久化记录每个视图的可见性与匿名性st_tables的is_public列同文件 L329记录表级可见性。宏层强制约束crates/bindings-macro/src/view.rs 强制视图必须声明public从编译期杜绝私有视图这一当前尚未支持的形态#[spacetimedb::table]宏在 crates/bindings-macro/src/table.rs 解析public标志。最佳实践小结私有是默认除非客户端确实需要读否则保持表为私有把敏感字段密码哈希、密钥、薪资等全部放在私有表中写操作只走服务端无论表是否公开所有写入都收敛到 reducer或显式事务的 procedure客户端永远只读用 View 做细粒度裁剪行级过滤依赖ctx.sender 索引查找列级隐藏依赖自定义投影类型两者可组合优先匿名视图能用AnonymousViewContext就尽量用物化一份结果共享给所有订阅者避免为每个用户重复计算给过滤字段建索引View 只允许索引查找按identity、sender等字段过滤前务必建立唯一索引或 BTree 索引procedure 慎用仅当需要 reducer 之外的特性如对外 HTTP 请求时才用 procedure且事务闭包须保持无副作用、可重复执行。相关延伸阅读Procedures 完整文档、Views 完整文档、Subscriptions 完整文档以及本目录下的 索引 与 约束 等表相关主题。【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表