ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

OnlyOffice动态权限API实战:实时将协作者转只读

2026/9/29 3:22:30 拓冰建站 浏览量
OnlyOffice动态权限API实战:实时将协作者转只读 我们团队把在线文档从 WPS 切到 OnlyOffice 之后第一个被产品经理盯上的需求就是能不能像 WPS 那样管理员一个按钮把某个正在改文档的协作者实时变成只读。OnlyOffice 的编辑器本身开放能力并不弱初始化时就能在 config 里传 permissions 配置每个打开文档的人可以是编辑、评论、审阅或只读。但这套权限有个隐坑——它是“打开时定稿”的。用户在编辑器里点开文档的一瞬间权限就已经固定下来后面想收回编辑权常规做法只能让那个人重新打开文档既不友好也没法做到“实时”。如果不刷新页面就能收回很多人下意识觉得实现不了。其实 OnlyOffice 提供了一个很容易被忽略的动态权限命令executeCommand(setPermissions)它能在编辑器已经打开的情况下实时调整当前会话的编辑、下载、打印等能力效果上相当接近 WPS 协作里的“转只读”操作。这篇文章我就把这套动态权限API从部署、初始化到命令调用、服务端配合、踩坑排查整个链路讲清楚。适合打算把 OnlyOffice 集成到自有系统里、又不想被权限设计坑害的后端同学也适合正在给协作文档做“管理员强制只读”这类功能的前端和全栈工程师。1. 动态权限API到底解决了什么问题1.1 静态 permissions 配置的局限OnlyOffice 在打开文档时需要在初始化配置里塞一个 permissions 对象控制这个编辑器实例里的人到底能干什么。典型的字段有这些权限字段作用说明edit是否允许编辑false 时打开就是只读模式download是否允许下载控制下载原文件按钮print是否允许打印false 时隐藏打印按钮review是否允许审阅模式控制修订/审阅能力comment是否允许批注false 时不显示批注工具fillForms是否允许填写表单对 xlsx/form 文档有效modifyFilter是否允许修改筛选器主要针对表格文档这套静态 permission 适合的场景是文档权限在打开那一刻就确定了比如普通人看合同、财务改表格、秘书批注流程稿。它的问题也显而易见——用户一旦把编辑器打开了权限就绑定在这个会话上后端数据库里哪怕把他改成只读浏览器里那个人照样能继续打字。我们之前遇到一个真实事故运营同事把一份活动方案发给供应商协作过程中发现对方在乱改管理员只能把对方整体踢出去但踢之前对方已经回车了一段错误内容。踢出去再重新登录是能解决可是正在编辑的那个人还需要刷新页面如果他那里有未保存的操作还会弹一堆提示体验很割裂。1.2 动态命令的价值不动页面即时收回动态权限API就是为了解决“已经打开的编辑器如何重新分配权限”这个问题。核心是executeCommand系列包括调整权限、关闭文档、刷新历史记录等。举个直观例子window.docEditor.executeCommand(setPermissions, { edit: false, comment: false, download: false, print: true });这段代码执行后当前连接的编辑器会立刻进入“只能看、不能改”的状态编辑工具栏禁用、右键菜单的修改项消失、保存按钮变灰。用户不需要刷新不需要重新打开文件视觉上就像 WPS 里被管理员切换成了“查看模式”。这里的实时收回不是 WPS 云端那种“账号级别踢下线”而是会话级别的。其优点是可以做到精细控制——文档里可能还有别人需要继续编辑不需要一杆子打死整份文档。1.3 为什么不直接改 config 里的 permissions有同学会问我重新调用一次new DocsAPI.DocEditor给一个新的 permissions 配置是不是也能达到效果能但代价很大。重新初始化编辑器意味着整个 iframe 要重建用户正在输入的内容、光标位置、滚动条位置全部丢失。多人协作时重新加载还会导致其他用户看到“有人进出文档”的通知甚至出现连接占用冲突。动态命令是在现有编辑器实例上操作几乎是无感的。所以理解两者的关系很重要——静态 permissions 决定“打开时默认是什么角色”动态setPermissions决定“编辑过程中能不能中途换角色”。两者配合才能做出真正可用的权限系统。2. 部署与初始化动态权限API的运行前提2.1 Docker 部署 OnlyOffice Document Server要玩动态权限API首先得有一台 OnlyOffice Document Server。我强烈建议自己部署而不是用公共 demo因为权限接口、回调地址、JWT 密钥这些都是要绑定自己域的。最省事的部署方式是 Docker。官方镜像onlyoffice/documentserver支持一键起服务但目录映射和资源配置不能随便糊弄。我实际使用的基础命令是sudo docker run -d -p 80:80 -p 443:443 --restartalways \ -v /opt/onlyoffice/logs:/var/log/onlyoffice \ -v /opt/onlyoffice/data:/var/www/onlyoffice/Data \ -v /opt/onlyoffice/db:/var/lib/onlyoffice \ -v /opt/onlyoffice/cache:/var/lib/onlyoffice/db/cache \ -e JWT_ENABLEDtrue \ -e JWT_SECRETmy_strong_secret_key \ --name onlyoffice-ds \ onlyoffice/documentserver:latest这里特别提示数据目录一旦跑起来就不要乱动尤其是/var/lib/onlyoffice/存放了文档缓存和 PostgreSQL 数据删掉等于把之前在线编辑的历史记录全部清空。另外JWT_SECRET必须记好后面前端生成编辑器配置 token、后端验证回调时都要用到同一个密钥。启动后访问http://your-ip/healthcheck如果返回一串true字符说明服务正常了。我第一次部署时卡在这里很久排查才发现是 80 端口被 nginx 占用了导致的假死现象。如果遇到端口冲突建议把外部端口映射成 8080、8443 这类非常规端口但注意前端集成时跨域策略要跟着调整。2.2 Vue3 项目里初始化编辑器OnlyOffice 官方没有 React/Vue 二开封装得特别舒服的包最常见的方式是自己管理生命周期在页面里引入 api.js然后创建编辑器。以 Vue3 为例初始化代码大概是这样的// 引入文档服务器上的 api.js // script srchttp://your-oc-server/web-apps/apps/api/documents/api.js/script const globalDocEditor ref(null); function openDocument(fileUrl, fileKey, userInfo, docPermissions) { const config { document: { fileType: docx, url: fileUrl, title: 合作协议.docx, key: fileKey, permissions: { edit: docPermissions.edit ?? true, download: docPermissions.download ?? true, print: docPermissions.print ?? true, comment: docPermissions.comment ?? true, review: docPermissions.review ?? false, } }, documentType: word, editorConfig: { lang: zh-CN, callbackUrl: https://api.yourapp.com/callback, user: { id: userInfo.id, name: userInfo.name, } }, token: generateEditorToken(fileKey, userInfo, docPermissions) }; globalDocEditor.value new DocsAPI.DocEditor(editor-placeholder, config); }这里的token不是登录 token而是用 OnlyOffice 的 JWT 规则对 config 对象签名后得到的字符串用来防止用户篡改权限。也就是说用户传过来的permissions如果和 token 不一致文档服务器会拒绝加载。一个容易踩坑的地方文档 key 的生成。OnlyOffice 会拿 key 当文档缓存的标识。同一个文件不管打开多少次key 应该保持一致这样历史记录和协同状态才能串起来。如果你每次打开都重新随机一个 key等于每次都是“新文档”动态权限、历史版本全部对不上。2.3 集成时需要知道的访问域名问题OnlyOffice 编辑器默认会对 document.url 做域名校验如果文档服务器的地址和打开页面的域名不一致会拒绝加载或白屏。开发环境最常见的问题就是这个。解决办法有两个方向把文档服务要访问的域名加到 Document Server 白名单官方支持通过环境变量配置更省心的是让前端页面的域和 OnlyOffice 服务的域一致或者用反向代理把两者统一。我自己用的是 Nginx 代理/onlyoffice路径转发到文档服务器前端和编辑器都在同一个主域下跨域问题直接消失。3. setPermissions 命令的实操拆解3.1 命令语法与字段选择动态权限最重要的一条命令就是setPermissions调用方法统一走executeCommanddocEditor.executeCommand(setPermissions, { edit: false, download: false, print: true, review: false, comment: false, fillForms: false, modifyFilter: false });刚开始我把这个命令当成“全量替换权限”后来发现它更像是“当前会话的状态机切换”。比如我想让当前用户只保留批注权可以这样docEditor.executeCommand(setPermissions, { edit: false, download: true, print: true, review: false, comment: true });执行后用户不能再修改正文但还能加批注。这个组合非常适用于“管理层审阅修改稿”的场景。各字段的生效范围不一样尤其要注意modifyFilter只对 xlsx 类型文档有意义写在 docx 里不会报错但也不会有感知变化。fillForms主要影响包含表单域的文档普通文档直接用不上。如果对这些字段理解不透上线后很容易出现“明明设置了权限按钮还在”的诡异现象。3.2 执行时机的掌握很多人调用setPermissions后没反应不是命令写错了而是调用时机不对。关键原则是必须等编辑器内部加载完再调用。如果用户刚打开页面api.js 还在初始化你直接executeCommand很多版本会直接报“editor is not defined”或者干脆静默失败。稳妥做法是在onAppReady回调之后执行。初始化时可以挂事件const docEditor new DocsAPI.DocEditor(editor, config); docEditor.on(onAppReady, () { // 此时才能安全执行命令 });另外setPermissions改变的是当前已连接会话的权限。用户新开一个标签页打开同一份文档还是会走初始配置的 permissions 和 token。这个认知很重要否则就会出现“我把某用户改成只读结果他关掉页面重新打开又变成可编辑了”的问题。3.3 动态权限对协同其他用户的影响很多协作功能是基于权限的比如历史记录、修订、批注。动态切换到只读后其他在线用户会感知到这个人的角色变化吗答案是OnlyOffice 的协同机制里人物状态会有一个“半实时”同步但如果想让对方的状态在聊天栏里立即显示为“只读查看者”还是建议配合服务端的用户信息推送。在实际多人编辑中如果 A 用户被动态切为只读他之前输入了一半的内容会保留在文档里但之后无法继续输入。如果当前那个位置还有未提交的改动编辑器不会自动帮他保存。所以“实时收回编辑权”业务上要处理“当前未保存内容”的问题不能只靠一条命令结束战斗。我自己在项目里的做法是强制只读前先触发一次保存动作把当前改动落盘然后再调用setPermissions。这样既保住用户的工作成果又完成了权限收口。4. 服务端同步与回调校验防止绕过是重中之重4.1 为什么不能只靠前端命令setPermissions只是前端命令它可以改变浏览器里的 UI 状态但无法改变后端托管在 OnlyOffice Document Server 里的真实验证逻辑。理论上一个懂行的人完全可以用控制台手工构造请求绕过前端限制去尝试保存。所以我在项目里反复强调动态权限API是“体验层”服务端校验才是“安全层”。两端必须配合。OnlyOffice 的保存过程会向后端配置的callbackUrl发一个回调请求里面带上 status、url、key、users 等信息。主要状态有这些status含义处理动作1一个用户开始编辑/文档有变动可以记录日志2文档已准备好保存根据 url 下载文件内容存到自己的存储3保存发生错误记录错误排查4用户关闭文档/连接断开可以清理会话记录如果只在浏览器里把按钮隐藏了但回调接口还无条件把所有保存请求都接受用户依然可以通过构造回调请求把内容提交上来。所以服务端必须维护一份“当前用户是否允许编辑”的名单在回调里校验调用者身份不允许编辑的用户抛异常。4.2 服务端强制只读的落地方式我用的方案是在生成编辑器配置的阶段就把“是否允许编辑”体现进 token 里。后端在构建 config 时根据用户权限生成对应的 token签名后返回前端。当用户被标记为强制只读后后端接口立刻拒绝给他签发“可编辑”的 token。伪代码大概这样def build_editor_config(request): user get_user(request) can_edit user.can_edit and not user.force_readonly permissions { edit: can_edit, download: True, print: True, comment: True, review: False, } config { document: { fileType: docx, url: file_url, key: file_key, permissions: permissions }, documentType: word, editorConfig: { lang: zh-CN, callbackUrl: callback_url, user: {id: user.id, name: user.name} } } config[token] sign_jwt(config, jwt_secret) return config动态权限命令负责让已经在浏览器里的人“现在立刻失去编辑能力”服务端 token 则保证他下次打开文档、保存文档时同样没有编辑权限。两个机制缺一个权限体系都不完整。4.3 保存回调里的校验就算文章是只读的OnlyOffice 也可能把一些状态变更的消息发到回调 URL比如 status1 表示有人打开了。所以不能简单地把“status2 才校验”。我的建议是回调接口统一做三件事。第一验证签名。JWT 签名不对直接返回{error:1}别做任何业务处理。第二根据 key 反查文档信息再根据回调请求里的用户 ID 判断该用户当前是否在强制只读名单里。第三如果用户被强制只读除非保存内容来自上一次可编辑期间的合法变更否则拒绝落库。更简单的做法是只记录日志不更新正式文档。实战中我踩过一个大坑用户先以编辑身份打开文档我后台把他改成只读但他在被改之前点了保存回调顺序是“保存成功 - 变只读”这不丢数据。但如果回调接口不做校验就会出现“用户已经被管理员封禁为只读结果之前点击保存的请求还在后台被正常处理”的问题数据更新的时间顺序完全乱套。所以服务端一定要以“当前时刻的权限”作为保存判断依据而不是请求发出时的权限。5. 像 WPS 一样完整实现“管理员收回编辑权”5.1 前后端联动的完整流程要模拟 WPS 那种“管理员点一下对方马上变只读”的体验单靠前端写死按钮是不够的至少需要一条推送链路。我给一个参考工作流管理员在后台系统点击“强制只读”按钮后端更新数据库里的用户权限状态标记force_readonlytrue后端通过 WebSocket 或消息推送到对应用户的浏览器会话因为文档服务器和后端业务服务是两个系统一般走自己的 Socket 服务用户浏览器收到消息后调用docEditor.executeCommand(setPermissions, {edit: false, ...})同时前端界面上弹出一个小提示比如“管理员已将你切换为只读模式”用户后续任何打开文档的请求后端生成 token 时都不会再给edit: true。推送消息可以设计成{ type: force_readonly, fileKey: agreement-2025, reason: 文档已进入收稿阶段, timestamp: 1731234567890 }客户端收到后执行socket.onmessage (event) { const msg JSON.parse(event.data); if (msg.type force_readonly) { docEditor.executeCommand(setPermissions, { edit: false, comment: false, download: true, print: true }); showToast(管理员已将你切换为只读); } };这套流程做完体感上已经很接近 WPS 的实时权限回收了。5.2 只针对单个用户而不是所有人setPermissions这条命令有个特点它是对当前编辑器实例生效的。也就是说如果一个文档同时开着多个用户的编辑器你在某个用户浏览器里执行命令只会让这个实例变成只读不会影响其他人。这个特性其实是好事可以做精确控制。管理员想踢的是 A 用户那就只往 A 的 WebSocket 发消息其他协作者的编辑器不受影响。如果所有用户的编辑器都被执行了setPermissions那就相当于整篇文档变成了“全局只读”。记得维护一份在线用户与会话 ID 的映射关系。OnlyOffice 的用户标识是用editorConfig.user.id传过去的我们后端推送时可以根据这个 ID 找到对应的 WebSocket 连接实现精准定向。5.3 历史记录和批注权限的细节动态权限里比较容易忽略的是历史记录。如果用户从可编辑变成只读他想查看历史修改记录很多版本默认是允许的。业务上如果“只读”不代表“可以追溯”就可能出现越权看到敏感修改过程的情况。想彻底一点可以把 review 也置为 false。因为历史记录在 OnlyOffice 里通常跟审阅权限挂钩。设置示例docEditor.executeCommand(setPermissions, { edit: false, review: false, comment: false });批注是另一个重灾区。把 edit 设置成 false 之后用户可能还能新建批注。有些场景这是合理的收稿后仍希望收集意见但如果你希望彻底清空对方的操作能力一定要把 comment 也设置成 false。顺带回答一个经常被问到的问题能不能通过 API 直接拿到 OnlyOffice 里的批注列表实践中没有特别优雅的官方开放接口一般靠两种方式一是开放导出把文档导出成带批注的文件再人工解析二是改造会话把批注数据通过回调或额外存储同步出来。如果项目要求比较轻我会优先让用户在 OnlyOffice 界面里管理批注不要过度承诺 API 能力。6. 常见问题排查与避坑实录6.1 权限命令不生效的几种原因症状常见原因验证方法executeCommand 报错编辑器还没就绪检查是否在 onAppReady 之后调用设置了 edit:false但双击文字还能编辑权限字段传入错误确认字段名是edit不是editable表格里还能筛选忘记设置 modifyFilter对 xlsx 需补modifyFilter: false只读后还能下载原文件没把 download 设为 false检查命令参数用户重新打开文档又恢复可编辑初始配置 permissions 仍为 true必须改服务端 token 和初始 config初期集成时最常见的问题就是把动态命令当作“服务端权限修改”。其实它只是改前端会话状态后端不跟着改重开就恢复了。一定要记住这句结论动态权限只是临时状态真正的持久权限在服务端生成配置和回调校验里。6.2 部署与 JWT 的深坑只要涉及 OnlyOfficeJWT 绝对是最大坑位。前端生成 config token、文档服务器配置 JWT、后端校验回调这三个环节必须用同一个 secret少一个都会出问题。我遇到过Invalid token错误第一反应是检查 JWT_SECRET 是否一致结果不是。真正原因是前端生成的 token 是用 JSON.stringify(config) 签名的而我在后端写回调校验时错误地把整个 config 又包了一层。所以调试这类问题别急着怀疑环境先用官方推荐的方式确认签名原材料是不是同一个对象结构。Docker 部署时还有一个经验动态权限命令如果发现“没有任何反应”先检查文档服务器和你的业务系统是不是同域。跨域 cookie、iframe 安全策略都会导致 JS 执行不到编辑器实例上。6.3 线上事故案例误把所有人踢出去有次我们环境出问题时我为了方便全项目测试直接写了个接口对所有在线用户执行executeCommand(close)想着模拟“文档被强制关闭”。结果那条命令把整份文档的所有编辑会话直接关掉了正在编辑的人弹了保存提示没保存的内容差点丢。如果只是想“收回编辑权”千万不要用 close 来代替 setPermissions。close是结束整个编辑会话没有后续重新授权机制setPermissions才是在保留会话的前提下调整能力。顺序应该是先触发保存 - 再执行 setPermissions - 如果实在需要强行关文档再考虑 close。6.4 性能与缓存问题动态权限API本身性能开销很小真正影响体验的是文档服务器缓存。比如你给用户切回只读后他重新打开文档还是能改很多时候不是权限没生效而是文档缓存让他读到了旧配置。解决办法是在服务端生成新的文档 key。只要 key 变了OnlyOffice 会认为这是一次新会话重新获取配置。但要谨慎key 一变历史记录和协同上下文也会重置所以这不是随意频繁使用的操作。内存方面OnlyOffice 容器在多人并发打开文档时容易吃满。权限动态切换不会显著增加内存压力但如果你的服务器只有 2G 内存建议把内存上限调到 4G否则会出现文档打开到一半卡死权限命令也执行不了的连锁故障。7. 落地设计的一些个人建议跟动态权限API打交道这么久我的体会是不要把setPermissions当成一个单独的魔法接口它应该被设计成“权限服务”的一部分。你需要先有后台的角色管理系统、用户在线状态系统、WebSocket 推送机制然后再用 OnlyOffice 的这套命令去落地交互层。我给后来者的建议是在开发环境把“管理员强制只读”功能做成一个调试按钮放在编辑器页面的侧边栏。它的价值不止是演示更在于能快速验证每个权限字段的真实效果。我当年就是靠这个按钮才搞清楚modifyFilter和comment在不同文档类型上的差异。如果项目里还有 Moodle 或 Vue 这类集成场景优先把 token 生成逻辑抽成公共服务不要散落在各个页面。动态权限是一条命令的事但围绕它的业务闭环往往会牵连到文档 key、回调地址、用户白名单、历史版本等一串设计。把这些基础打好后续再做单项权限回收、批量回收、定时自动回收都会顺畅很多。