身分與授權
透過 Transport、Endpoint 和 Service Middleware 接入既有驗證與授權系統。
Univer Collaboration SDK 不定義使用者、角色或 ACL 模型。應用在 Transport Middleware 中建立可信身分,再透過 Endpoint 和 Service Middleware 保護即時 Session 與以服務端為準的協同資料。
本頁是 Middleware 與 Events 的具體應用,著重介紹如何透過 Middleware 接入身分與授權。
驗證編輯與唯讀權限
停止前一範例並執行 pnpm example:permissions。兩個獨立瀏覽器工作階段開啟 http://127.0.0.1:3010,分別登入 Alice · Editor、Bob · Viewer。前者可修改,後者可讀取同步結果但不能修改。依 server/main.ts 檢查 transport.use() 身份解析、readUnitData 讀取權限、joinUnit 房間權限及 submitChangeset 編輯權限。以真實認證及文件 ACL 取代示範 Cookie 與使用者列表;前端唯讀並非權限邊界。
頁面還提供 Casey · No access。使用此帳號登入後應拒絕載入文件,snapshot 請求返回 HTTP 403。
身分模型
| 識別碼 | 提供方 | 意義與生命週期 |
|---|---|---|
userID | 應用 | 穩定的業務身分,也是已確認變更集的作者 |
memberID | Endpoint | 一個 WebSocket Session 的線上成員 ID;重新連線後會改變 |
sid + reqId | 用戶端 | 變更集提交識別碼,在重試期間保持不變 |
瀏覽器提交的使用者資料、memberID 和修訂版本不能代替應用驗證。請將應用的穩定使用者識別碼對應到 context.userID。
在 Transport Middleware 中建立身分
Transport 對每個協同 HTTP 請求重新執行 Middleware。應用可讀取 Cookie、Session 或 Bearer token,並將驗證結果加入 Context:
transport.use(async (context, next) => { const user = await auth.requireUser(context.incomingMessage); context.userID = user.id; context.customData.user = user; context.customData.tenantID = user.tenantID; context.customData.traceID = readTraceID(context.incomingMessage); await next();});驗證失敗時,結束 HTTP 回應,不呼叫 next():
transport.use(async (context, next) => { const user = await auth.findUser(context.incomingMessage); if (!user) { context.response.statusCode = 401; context.response.end("Authentication required"); return; } context.userID = user.id; await next();});SDK 不規定 Cookie 名稱、token 格式、使用者資料表或登入流程。
身分如何進入 WebSocket Session
WebSocket 流程不信任用戶端資料中的使用者欄位。Endpoint 透過一次性 Session Ticket,將已驗證的 HTTP Context 延伸到即時 Session:
Session Ticket HTTP 請求 → Transport Middleware 驗證身分 → Endpoint 儲存 { userID, customData } → 傳回不透明的一次性票證 → WebSocket 開啟時消耗票證 → 建立 Session { userID, memberID, customData }票證字串不包含 userID 或 customData。memberID 只識別目前連線,不是獨立的身分憑證。多節點部署使用共用的票證儲存時,customData 會被序列化保存,應保持可 JSON 化。
在正確的邊界授權
| 需要保護的行為 | Middleware |
|---|---|
| 協同 HTTP 入口 | Transport use() |
| WebSocket 連線 | Endpoint connect |
| 加入 Unit Room | Endpoint joinUnit |
| 讀取快照、區塊或變更集 | Service readUnitData |
| 提交內容編輯 | Service submitChangeset |
| 建立 Unit | Service createUnit |
| 刪除 Unit | Service deleteUnits |
| 還原 Unit | Service recoverUnits |
Endpoint joinUnit 只控制 Session 能否進入即時 Room。快照與缺少的變更集可直接透過 HTTP 讀取,因此 JOIN 檢查不能代替 readUnitData。同樣地,用戶端唯讀 UI 只是產品提示,不能代替服務端 submitChangeset 策略。
保護讀取、JOIN 與編輯
import { CollabError } from "@univerjs-pro/collaboration-service";endpoint.use("joinUnit", async (context, next) => { const allowed = await acl.canRead(context.session.userID, context.unitID); if (!allowed) { throw new CollabError("PERMISSION_DENIED", "Cannot join this Unit"); } await next();});service.use("readUnitData", async (context, next) => { const allowed = await acl.canRead(context.userID, context.request.unitID); if (!allowed) { throw new CollabError("PERMISSION_DENIED", "Unit is not accessible"); } await next();});service.use("submitChangeset", async (context, next) => { const unitID = context.request.changeset.unitID; const allowed = await acl.canEdit(context.userID, unitID); if (!allowed) { throw new CollabError("PERMISSION_DENIED", "Unit is read-only"); } await next();});實際應用通常同時安裝這三項規則:HTTP 和 JOIN 的讀取策略,以及提交的編輯策略。
保護 Unit 生命週期
協同協定開啟既有 Unit。「建立文件」通常是應用 API:先建立產品記錄與 ACL,再呼叫 createUnitFromData() 或 createUnitFromSnapshot()。也應透過 Service Middleware 保護生命週期:
service.use("createUnit", async (context, next) => { if (!(await acl.canCreate(context.userID, context.request.snapshot.type))) { throw new CollabError("PERMISSION_DENIED", "Unit creation denied"); } await next();});service.use("deleteUnits", async (context, next) => { for (const unitID of context.request.unitIDs) { if (!(await acl.canDelete(context.userID, unitID))) { throw new CollabError("PERMISSION_DENIED", `Cannot delete ${unitID}`); } } await next();});單獨保護 recoverUnits。曾有刪除權限的使用者,不一定永遠保有還原權限。
透過 customData 重用查詢結果
同一次 Service 呼叫中的 Middleware 可用 customData 避免重複查詢:
service.use("submitChangeset", async (context, next) => { const unitID = context.request.changeset.unitID; context.customData.role ??= await acl.getRole(context.userID, unitID); if (context.customData.role === "viewer") { throw new CollabError("PERMISSION_DENIED", "Unit is read-only"); } await next();});customData 只屬於目前呼叫或 Session,不會自動持久化。角色、ACL 與租戶關係仍應儲存在應用中。
擴充模組需要獨立策略
History、Thread Comment 和 Worktree 有獨立的 Service Middleware:
- History 清單與變更集讀取應檢查 Unit 讀取權限;
- 留言清單應檢查讀取權限,新增、回覆、編輯與刪除則套用留言策略;
- Worktree 應分別保護可見性、草稿編輯、狀態變更與合併;
- Worktree 合併到主幹仍會進入主幹的 Service Middleware。
核心 Collaboration Service 上的規則不會自動保護這些模組。請重用同一個應用策略服務,而非為各擴充建立獨立 ACL 資料模型。
在用戶端設定目前使用者
UserManagerService.setCurrentUser() 設定目前 Univer 執行個體的使用者 ID、名稱與頭像,供前端元件讀取。例如,留言面板的「與我相關」篩選器使用此 ID 顯示與目前使用者相關的留言。
在建立 Univer 執行個體後、載入文件前設定:
import { UserManagerService } from "@univerjs/core";// currentUser is your application profile; id/name/avatar are example field names.univer .__getInjector() .get(UserManagerService) .setCurrentUser({ userID: currentUser.id, name: currentUser.name, avatar: currentUser.avatar ?? "", });將應用的穩定使用者 ID 對應到 userID,與服務端已驗證的 context.userID 一致。協同中共享的成員名稱與頭像,應在服務端 connect Middleware 中設定,詳見 Presence。
完整可執行程式請參閱權限範例。
你覺得這篇文檔如何?