身分與授權

透過 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應用穩定的業務身分,也是已確認變更集的作者
memberIDEndpoint一個 WebSocket Session 的線上成員 ID;重新連線後會改變
sid + reqId用戶端變更集提交識別碼,在重試期間保持不變

瀏覽器提交的使用者資料、memberID 和修訂版本不能代替應用驗證。請將應用的穩定使用者識別碼對應到 context.userID。

在 Transport Middleware 中建立身分

Transport 對每個協同 HTTP 請求重新執行 Middleware。應用可讀取 Cookie、Session 或 Bearer token,並將驗證結果加入 Context:

TypeScript
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():

TypeScript
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:

text
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 RoomEndpoint joinUnit
讀取快照、區塊或變更集Service readUnitData
提交內容編輯Service submitChangeset
建立 UnitService createUnit
刪除 UnitService deleteUnits
還原 UnitService recoverUnits

Endpoint joinUnit 只控制 Session 能否進入即時 Room。快照與缺少的變更集可直接透過 HTTP 讀取,因此 JOIN 檢查不能代替 readUnitData。同樣地,用戶端唯讀 UI 只是產品提示,不能代替服務端 submitChangeset 策略。

保護讀取、JOIN 與編輯

TypeScript
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 保護生命週期:

TypeScript
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 避免重複查詢:

TypeScript
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 執行個體後、載入文件前設定:

TypeScript
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。

完整可執行程式請參閱權限範例。

你覺得這篇文檔如何?

© 2026 DreamNum Co., Ltd.