如果你的系统里躺着一堆 Word、Excel、PPT 文件,用户今天要在线预览,明天要协同编辑,后天又要权限控制,你第一个冒出来的方案是什么?两年前我接手办公系统的文档模块时,想的也是先把文件转 PDF 扔到浏览器里。结果只撑了两周就被业务部门投诉了:PDF 只能看不能改,合同批注得下载到本地,改完再传回来,版本乱成一锅粥。后来我决定找一套能直接嵌进 SpringBoot 服务里的在线编辑方案,筛选到最后剩两个选择:Collabora Online 和 OnlyOffice。考虑到我们团队 Java 技术栈比较熟、不想额外维护一套 Node 服务,最终选了 Collabora Online,基于 WOPI 协议自己写服务端适配,把在线编辑能力彻底并进了现有权限体系里。
这篇文章就把整个集成过程拆开讲清楚:包括 WOPI 协议的核心机制、Collabora Server 的容器化部署、SpringBoot 后端四个关键端点的实现、前端接入方式,以及从"能打开"到"能稳定上线"过程中我踩过的那些坑。无论你是刚开始接触在线编辑的新手,还是已经折腾过 OnlyOffice 想换方案的老手,这篇应该都能给你省下不少弯路。
1. 为什么在线编辑要用 Collabora Online:先聊聊选型和架构
1.1 选型的真实考量:不是 OnlyOffice 不好,而是有些事绕不开
先说结论:OnlyOffice 和 Collabora Online 都是成熟方案,选哪个不取决于功能强弱,而取决于你的技术栈和部署环境。我当初做选型对比时列了一张表,把关键差异一张图说明白:
| 对比维度 | Collabora Online | OnlyOffice |
|---|---|---|
| 后端语言 | C++(核心),WOPI 接口标准 | Node.js + C++ |
| 与 SpringBoot 集成 | 自己实现 WOPI 端点,控制力强 | 官方提供 Java 示例,但文档较散 |
| 部署形态 | 单一 Docker 镜像,依赖少 | 需要 DocumentServer 镜像,内存占用偏高 |
| 协同编辑体验 | 基于 LibreOffice 核心,格式兼容性好 | 自带格式转换引擎,对 docx 兼容性强 |
| 二次开发门槛 | 对接 WOPI 协议,概念清晰 | 需要理解回调 API 和 JWT 机制 |
我们当时的实际情况是:核心业务系统全部跑在 SpringBoot 上,运维只愿意多开一个容器,不想引入 Node.js 全家桶。Collabora Online 的 WOPI 协议把"文档查看编辑"和"文件存储管理"彻底解耦,我只需要在后端实现标准接口,Collabora Server 负责渲染和编辑,这就意味着存储逻辑、权限校验、版本管理都能复用现有业务代码。这一点对我们来说价值最大。
1.2 Collabora Online 的两种工作模式:别把"自托管"和"云服务"搞混
Collabora Online 分为两类:Collabora Online(商业版)和 Collabora Online Development Edition(CODE,社区开发版)。社区版功能上已经覆盖了在线查看、编辑、协同,支持 Writer、Calc、Impress,对于大多数业务系统足够用。官方也出 Docker 镜像collabora/code,部署非常简单,后面会细讲。
另外有个容易被忽略的点:Collabora Online 依赖 LibreOffice 内核做文档解析和渲染,所以它对 ODF(Open Document Format)的支持很原生,对 docx、xlsx、pptx 的兼容性也不错,但跟微软 Office 的像素级还原比还是有差距。如果你的用户天天拿精密排版的长文档说话,这个预期得提前对齐。对我们来说,内部 OA 系统里大多是合同、审批单、报表,这种兼容性完全够用了。
1.3 架构全貌:客户端、Collabora Server、你自己写的后端各管什么
整个链路可以简化成三步:
- 用户在浏览器打开你的业务系统页面,前端通过 iframe 加载 Collabora Server 的编辑页地址;
- Collabora Server 拿到 URL 里携带的 WOPI 文件标识和令牌后,向后端发送一系列 WOPI 请求,获取文件信息、文件内容、保存文件内容;
- 后端在响应这些请求时做身份校验、权限判断和读写存储。
注意这里有个关键点:Collabora Server 本身不保存文件,它只是"借用"你的后端来读写文件。文件真正落盘的地方是你的服务器、OSS、MinIO,或者数据库。这种松耦合设计带来的直接好处是:文件永远只存在于你自己的存储体系内,安全可控,而且很容易接入已有的审计流程。
这个架构同样带来一个学习成本:你必须把 WOPI 协议搞明白。别急,下一节我用最简单的话把协议说透。
2. 先搞懂 WOPI 协议:不掌握这一个概念后面全是坑
WOPI 全称 Web Application Open Platform Interface,翻译过来是"Web 应用开放平台接口"。本质是一组 HTTP 接口约定,文档编辑端(Collabora)作为 WOPI 客户端,你的 SpringBoot 服务作为 WOPI 服务端,双方通过 REST 风格的请求互相通信。
2.1 WOPI 的一问一答:CheckFileInfo、GetFile、PutFile 是什么
WOPI 定义了多个端点,但日常集成真正需要实现的核心端点就这几个:
CheckFileInfo:Collabora 打开文件时最先调用的接口。请求方式是GET /wopi/files/{fileId},后端返回一个 JSON 对象,里面描述了这个文件的所有元信息:文件名、大小、当前用户是否有编辑权限、文件版本号、最后修改时间等。Collabora 拿到这个 JSON 后决定展示成只读模式还是编辑模式。GetFile:GET /wopi/files/{fileId}/contents,返回文件的二进制内容。PutFile:POST /wopi/files/{fileId}/contents,把编辑后的文件内容写回后端。GetLock/RefreshLock/Unlock:编辑模式下的文件锁管理,防止多人同时写同一个文件造成覆盖。
看到这个结构你可能会想:这不就是文件 CRUD 接口嘛。对,本质确实是。WOPI 的作用是把"文档内容"和"编辑 UI"分开,双方只需要遵循统一接口,就能无缝配合。
2.2 理解锁机制:多个用户一起改同一个文件时靠什么兜底
锁是 WOPI 协议里最容易忽略但最重要的部分。Collabora 在进入编辑模式前会对文件加锁,锁的值是一个X-WOPI-Lock头,通常是一串 UUID。后续的 PutFile、RefreshLock 请求都要带上这个锁标识。
后端在保存文件时必须校验锁:如果请求头里的锁与当前文件持有的锁不一致,就返回409 Conflict,Collabora 会提示用户文件已被其他人修改。这个机制避免了很多并发写冲突。我之前遇到过一个问题:用户编辑完点保存,文件内容没丢,但 Collabora 经常弹"文件已更改"的提示,后来排查发现就是锁校验没做好,后面踩坑部分再展开。
2.3 Discovery XML:Collabora 是怎么知道该把文件交给谁处理的
Collabora Server 有一个"能力宣告"文件,通过http://你的collabora服务器:9980/hosting/discovery就能拿到。里面是一个 XML,列出了支持的文件扩展名、对应的 MIME 类型、以及每种文件类型应该拼接的编辑地址模板。
后端要做的事是:首次启动或定时去拉取这个 XML,缓存在内存里。当用户点击某个文档时,后端根据文件扩展名找到对应的urlsrc模板,把文件 ID、令牌等参数拼进去,生成一个完整地址返回给前端。
这个机制我刚开始觉得多此一举,后来发现好处很大:Collabora 升级后如果新增了文件类型支持,后端只要重新拉取 discovery 就行,代码完全不用动。
3. 环境准备:用 Docker 把 Collabora Server 跑起来
3.1 容器部署与关键参数说明
部署 Collabora Online Development Edition 最简单的方式是 Docker。生产环境建议固定版本号,不要用 latest,这样升级可控。以下是我实际使用的启动命令:
docker run -d \ --name collabora \ -p 9980:9980 \ -e "username=admin" \ -e "password=你的密码" \ -e "ServerName=your.server.domain" \ -e "DONT_ENFORCE_HTTPS=true" \ -e "extra_params=--o:ssl.enable=false --o:ssl.termination=true" \ collabora/code:22.05几个参数解释一下:
username和password:Collabora 的管理后台登录凭据,访问/browser/dist/admin/admin.html可以查看在线连接情况。别用弱密码。ServerName:必须设置为部署 Collabora 服务器的实际域名或 IP,Collabora 启动时会做 host 校验。这里最容易踩坑:如果填 localhost 但你在另一台机器上访问,服务会报 404 或者 403。DONT_ENFORCE_HTTPS=true:仅用于开发环境跳过 HTTPS 检查。生产环境必须配置反向代理启用 HTTPS,否则编辑功能基本没法稳定用,浏览器混内容策略和 Collabora 自身的安全限制都会出来找麻烦。extra_params这里的--o:ssl.enable=false --o:ssl.termination=true表示后面由 Nginx 之类的反向代理负责 TLS 终止,Collabora 本身只用 HTTP。这是一种很常见的部署模式。
3.2 验证服务是否正常:编辑页面不带令牌也能看个大概
启动之后,先在浏览器访问http://your.server.domain:9980/loleaflet/dist/loleaflet.html,如果能看到 Collabora 的欢迎页或者报错页面(因为缺少 WOPI 参数),说明容器基本起来了。接着访问 discovery 地址确认 XML 能正常返回:
curl http://your.server.domain:9980/hosting/discovery看到 XML 内容里有<net-zone name="test" ...>之类的节点,就说明服务正常。此时先别急着做业务对接,用官方提供的一个简单 WOPI 测试页面试一下编辑流程,确认容器本身没问题,再进入后端开发。这一步能帮你把问题范围缩小,省得像无头苍蝇一样排查。
3.3 反向代理配置:把 Collabora 对接到你现有的域名体系里
生产环境一般不会直接暴露 9980 端口,而是通过 Nginx 反代到统一域名下。需要注意 Collabora 的 WebSocket 支持,nginx 必须配置 Upgrade 头:
server { listen 443 ssl; server_name office.yourdomain.com; ssl_certificate /etc/nginx/ssl/your.crt; ssl_certificate_key /etc/nginx/ssl/your.key; # 静态资源 location /loleaflet { proxy_pass http://127.0.0.1:9980; proxy_set_header Host $host; } # WOPI 和其他 API 路径 location /hosting { proxy_pass http://127.0.0.1:9980; proxy_set_header Host $host; } # WebSocket 支持,协同编辑必须 location /coolgw/ { proxy_pass http://127.0.0.1:9980; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } }这里务必注意:/coolgw的 WebSocket 反代如果没配,协同编辑进入后容易出现白屏或者编辑状态不同步。我第一次上线就是漏了这段配置,排查了接近一天。
4. SpringBoot 后端:实现 WOPI 端点与发现配置
4.1 工程结构与依赖
后端我基于 SpringBoot 3.2 + Java 17,主要做两块事情:一是实现 WOPI 端点,二是写一个服务类负责 discovery 解析和地址生成。不需要引额外的第三方库,Spring MVC 自带的ResponseEntity和RestTemplate(或者WebClient)就够了。
工程结构大概这样:
com.example.office ├── controller │ └── WopiController.java # WOPI 核心端点 ├── service │ ├── WopiFileService.java # 文件读写与锁管理 │ ├── CollaboraDiscoveryService.java # 拉取和解析 discovery XML │ └── WopiTokenService.java # 令牌生成与校验 ├── model │ ├── WopiFile.java # 文件元信息模型 │ └── FileLock.java # 锁模型 └── config └── OfficeConfig.java # 读取 Collabora 地址等配置4.2 CheckFileInfo:决定文件以什么身份、什么权限打开
这是 Collabora 首先要调的端点。它的返回值决定了编辑器的展示状态:是只读还是可编辑、显示什么文件名、左上角面包屑导航显示什么。下面是一段核心实现:
@RestController @RequestMapping("/wopi/files") public class WopiController { @GetMapping("/{fileId}") public ResponseEntity<Map<String, Object>> checkFileInfo( @PathVariable String fileId, @RequestHeader("Authorization") String authHeader) { // 1. 校验令牌,从中解析出用户信息和文件权限 WopiToken token = wopiTokenService.parse(fileId, authHeader); // 2. 查询文件元信息 WopiFile file = wopiFileService.findById(fileId); Map<String, Object> info = new HashMap<>(); info.put("BaseFileName", file.getFileName()); info.put("Size", file.getSize()); info.put("OwnerId", file.getOwnerId()); info.put("UserId", token.getUserId()); info.put("UserCanWrite", token.hasPermission(fileId, Permission.EDIT)); info.put("UserCanNotWriteRelative", true); info.put("Version", file.getVersion()); info.put("LastModifiedTime", file.getLastModified().toInstant().toString()); info.put("BreadcrumbDocName", file.getFileName()); return ResponseEntity.ok(info); } }有几个字段我得专门提醒一下:
UserCanWrite:布尔值,控制编辑 UI 是否可用。如果用户在业务系统里只有查看权限,这里就要返回 false,Collabora 会显示只读视图,右上角的编辑按钮直接不出现。LastModifiedTime:格式必须是 ISO 8601,建议用 UTC 时间。时区问题会让 Collabora 误判文件是否被别人改过,从而弹出多余的版本冲突提示。OwnerId和UserId:Collabora 在 UI 上用来区分当前编辑者。如果两个用户用同一个 ID,协同编辑时会显示成同一人,容易造成"我改的字怎么没了"的错觉。
4.3 GetFile 与 PutFile:把文件内容安全地读进来、写回去
GetFile 实现简单,但一定要用流式方式写入响应体,别把整个文件 byte 数组一次性装载进内存。尤其对接 OSS 或 MinIO 的时候,用InputStreamResource或者StreamingResponseBody。
@GetMapping("/{fileId}/contents") public ResponseEntity<InputStreamResource> getFile( @PathVariable String fileId) { WopiFile file = wopiFileService.findById(fileId); InputStream inputStream = wopiFileService.openReadStream(file); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_TYPE, "application/octet-stream") .body(new InputStreamResource(inputStream)); }PutFile 稍微复杂一点。Collabora 会定期自动保存(默认大概 5 分钟一次,也会在用户关闭文档时触发),请求体就是完整的文件内容。后端要做的是覆盖写入。这里有两个关键点:
- 写完后返回 200 OK 即可,不需要返回内容本身。
- 如果文件被锁占用,必须返回 409 Conflict。
@PostMapping("/{fileId}/contents") public ResponseEntity<Void> putFile( @PathVariable String fileId, @RequestHeader("X-WOPI-Lock") String lock, HttpServletRequest request) { // 1. 校验锁是否匹配 if (!wopiLockService.isLockValid(fileId, lock)) { return ResponseEntity.status(HttpStatus.CONFLICT) .header("X-WOPI-Lock", wopiLockService.getCurrentLock(fileId)) .build(); } // 2. 流式接收文件内容并写回存储 wopiFileService.saveFile(fileId, request.getInputStream()); return ResponseEntity.ok().build(); }对,你没看错,锁校验后要把当前实际锁值放在响应头里返回。Collabora 会读取这个头来决定下一步怎么处理。这一步漏了,编辑保存时就会出现"文件版本已更改,该页面将被重新加载"的循环提示。
4.4 锁管理:后端需要一张锁表还是用内存就够了
锁的存储,小额场景可以用内存 ConcurrentHashMap,但生产环境如果有多实例部署就必须用 Redis。锁的本质就是一个fileId -> lockId的映射,附带过期时间。Collabora 好像也没有专门释放锁的可靠机制,文档关闭时可能因为网络原因通知不到,所以锁一定要设置合理的 TTL,我一般设 30 分钟,如果 30 分钟没有RefreshLock请求,锁自动失效。注意 Collabora 进入编辑模式后会周期性地发 RefreshLock,所以不会误伤正常用户。
@Service public class WopiLockService { private final StringRedisTemplate redisTemplate; public boolean isLockValid(String fileId, String lock) { String current = redisTemplate.opsForValue().get("wopi:lock:" + fileId); return current != null && current.equals(lock); } public void setLock(String fileId, String lock) { redisTemplate.opsForValue().set("wopi:lock:" + fileId, lock, Duration.ofMinutes(30)); } public void refreshLock(String fileId, String lock) { if (isLockValid(fileId, lock)) { redisTemplate.expire("wopi:lock:" + fileId, Duration.ofMinutes(30)); } } }这里有个细节:Collabora 的 GetFile 请求如果是因编辑而触发,也会带上X-WOPI-Lock头,所以初始化加载文件时就要把锁设好。我是这样处理的:CheckFileInfo里返回UserCanWrite=true时,同时生成 UUID 作为锁,写入 Redis,并在 GetFile 的响应头里返回该锁。如果UserCanWrite=false,则完全不设锁,Collabora 只在只读模式下工作。
4.5 发现配置与文件编辑地址生成:怎么把参数传给前端
后端需要缓存里保存 Collabora Discovery XML 的解析结果。我用一个@Scheduled任务每小时刷新一次:
@Component public class CollaboraDiscoveryService { private final Map<String, String> urlTemplateMap = new ConcurrentHashMap<>(); @Scheduled(fixedDelay = 3600000) public void refreshDiscovery() { RestTemplate restTemplate = new RestTemplate(); String xml = restTemplate.getForObject(collaboraUrl + "/hosting/discovery", String.class); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); // 解析动作节点,key 为扩展名,value 为 urlsrc 模板 // 例:<action ext="docx" ... urlsrc="http://.../loleaflet/dist/loleaflet.html?WOPISrc=..."/> urlTemplateMap.clear(); // ... 遍历填充 } }生成编辑地址时,需要把模板里的占位符替换成实际参数。Collabora 的 WOPI 地址模板核心参数大概长这样:
http://office.yourdomain.com/loleaflet/dist/loleaflet.html? WOPISrc=http://your-springboot-host/wopi/files/{fileId}& access_token={token}& access_token_ttl={ttl}注意WOPISrc指向的是你的 SpringBoot 服务地址,Collabora 后续发的 WOPI 请求都是以这个地址为基础的。千万别把WOPISrc配成 Collabora 自己的地址,否则它会去找自己身上的 WOPI 接口,什么都找不到。
access_token_ttl是令牌的过期时间,格式是毫秒时间戳。这个参数不要省,如果不传,Collabora 默认认为令牌永不过期,一旦你换用短期令牌,它不会主动刷新,编辑到一半就白屏。
5. 前端接入:让浏览器直接用上在线编辑
5.1 iframe 嵌入
前端接入其实非常轻量。后端提供了一个接口GET /api/documents/{docId}/edit-url,返回一段可直接 iframe 的地址。前端拿这个地址渲染即可:
<!DOCTYPE html> <html> <head> <style> html, body { height: 100%; margin: 0; } #officeFrame { width: 100%; height: 100%; border: 0; } </style> </head> <body> <iframe id="officeFrame" src="/api/documents/123/edit-url"></iframe> </body> </html>实际业务中,iframe 外层通常要做全屏布局,最好把顶部导航栏、侧边栏隐藏起来,避免用户在编辑文档时误触发业务系统的路由跳转。我见过一个项目把 iframe 放在一个需要滚动才能看完的区域里,Collabora 内部的鼠标滚轮编辑体验直接乱掉,用户骂声一片。
5.2 令牌的生成与刷新策略,别让用户编辑到一半被踢出来
这里聊一个很容易忽略的问题:access_token 是一次性生成、有有效期的,有效期到了 Collabora 并不会自动找你的后端换新令牌,它只会请求新的访问令牌或直接白屏。所以设计令牌有效期时要给足量。
我采用的做法是:令牌有效期设为 12 小时,令牌内容用 JWT 封装userId和fileId和permission信息。同时在后端配置 JWT 的签发密钥要注意统一,Collabora 回调不会用到密钥,它完全不关心 token 里装了什么,只负责把 token 原样传给后端,所以你可以完全自定义 token 格式,用 UUID 随机串也没问题。
真正重要的安全点是:这个 token 是直接出现在 URL 上的,不要放password之类的敏感信息,并且建议只在 HTTPS 环境下使用。
5.3 自动保存与用户退出提示
Collabora 默认会自动保存,但用户从编辑页跳走时,前端最好监听一下页面的 unload 事件,提示用户保存完成后退出,避免心理不安。另外,如果用户点击业务系统的菜单跳转到其他页面,iframe 销毁了,Collabora 那边的锁可能没法及时释放,所以我在后端加了一个定时兜底任务,每 10 分钟清理一次超过 30 分钟没刷新但当前又没有活跃编辑 session 的锁。
这里还可以提一个小技巧:在CheckFileInfo中返回一个CloseButton相关的属性(可选字段),可以在编辑页右上角显示关闭按钮,点击后回调你的页面。这个看起来不起眼,实际对用户体验提升很大,用户不用再通过浏览器返回按钮退出编辑。
6. 踩坑实录:从"能打开"到"稳定生产"的完整排查链路
6.1 "文件版本已更改,该页面将被重新加载":锁处理的经典翻车现场
这个提示是所有在线编辑接入者都会遇到的拦路虎。现象是:用户第一次打开文档编辑,保存也没问题,但第二次打开时,Collabora 报"文件版本已更改",然后强制刷新页面。
一开始我一度怀疑是 Collabora 的版本比对机制故障,后来抓包看 Collabora 发到后端的请求序列才明白问题所在。原来 Collabora 进入编辑页面时请求 CheckFileInfo 和 GetFile,CheckerFileInfo 返回了文件的Version字段。我把 Version 设计成自增整数,每保存一次就加一。第二次打开时,Version 已经从 1 变成 2,而 Collabora 因为上次编辑 session 在持久层还留着一个"文件状态记录",它对比到 Version 变了,就认为文件在编辑之外被外部程序修改过,于是提示重新加载。
解决办法有两种:
- 不频繁更新 Version 字段,除非业务上确实有"文件被另一个管理员手动覆盖"的场景,否则保持 Version 不变;
- 如果确实要更新版本号,则保证锁校验逻辑一致性,并确保在 PutFile 后返回的锁没有发生变化,让 Collabora 认为自己就是唯一的写入者。
后来我干脆把 Version 改成基于文件内容的哈希值,文件内容没变,Version 就不会变,这一下子解决了很多莫名奇妙的"版本冲突"提醒。说到底,WOPI 的 Version 字段语义是"文件是否被外部修改",而不是"文件保存了多少次"。
6.2 高版本 SpringBoot 带来的兼容性暗礁:javax 与 jakarta 命名空间
如果你的项目用了 SpringBoot 3.x,并且参考的是网上比较老的 Collabora 集成教程,大概率会碰上一个问题:老教程里的代码都是import javax.servlet.*,而 SpringBoot 3 已经把 Servlet API 迁移到了jakarta.servlet包。这个不是编译期必然报错的问题,有时候你复制老代码,IDE 自动导入了新包,但影响不大。真正危险的是你在配置拦截器或者过滤器时,引用了容器里的旧类,导致运行时ClassNotFoundException或者NoClassDefFoundError,服务启动后要等 Collabora 第一次调用 WOPI 接口时才暴露出来。
我的建议是:从接入一开始就直接按 SpringBoot 3.x 的jakarta命名空间写代码,不要想着兼容旧项目。另外 SpringBoot 3.2 自带的RestTemplate在设置超时时间时略有变化,记得用SimpleClientHttpRequestFactory显式设置连接和读取超时,避免 discovery 刷新时因为 Collabora 短暂不可用导致线程长时间挂起。
6.3 中文文件名与 URL 编码问题
这个坑特别隐蔽。当一个文件名是"年度汇报-2024.docx"时,Collabora 地址里的WOPISrc参数中如果没做 URL 编码,浏览器会截断地址,导致文件打不开。我一开始只在生成地址时对fileId做编码,后来发现 Collabora 回调时把WOPISrc原样带回来,后端反解时又出现 ISO-8859-1 中文乱码,文件名直接变成问号。
最后的解决方案是:整个WOPISrc参数做URLEncoder.encode(url, StandardCharsets.UTF_8.name()),后端接收时用URLDecoder.decode处理后再使用。前端 iframe 的 src 不要二次编码,否则服务器收到的地址和实际跳转的地址会不一致。
6.4 大文件的读取和写入:别让 JVM 堆内内存成为瓶颈
刚开始我用FileUtils.readFileToByteArray()直接读取文件再响应给 Collabora,结果运维同事跑来投诉说内存飙到 3G。原因很简单:一个 200MB 的 PPTX 文件,读取后 byte 数组在堆上占 200MB,加上 JVM 内部复制、网络缓冲,实际占用可能是好几倍。
改成流式操作之后情况立刻好转。GetFile 端点直接返回 InputStreamResource,PutFile 从请求的 InputStream 里读,边读边往 MinIO 写。这样即使并发编辑量上来,内存压力也基本可控。如果你的文件特别大(超过 500MB 这种),建议在 Collabora 的配置里限制一下最大可编辑文件大小,超限的提示用户下载处理,不要硬扛。
6.5 更多的坑:端口放行、管理后台、连接数监控
- 协同编辑需要 WebSocket,除了 9980 端口,还要放行 6114 和 6115(内部通信端口),如果是 Docker 部署单容器,这两个端口通常在容器内部就通,但如果用了某些云平台的安全组,记得查一下。
- Collabora 管理后台地址是
/browser/dist/admin/admin.html,可以看到当前活跃的编辑会话、内存占用、连接数。上线初期我每天看一遍,能直观感受到负载情况。如果在线用户多,注意观察下面这几项指标:peakRSS、sent、received。 - 如果 Collabora 服务器是海外的,编辑延迟会很高,尤其是首屏加载时。有条件的话,把 Collabora 容器部署在靠近业务服务器的位置,因为每次保存都是全量上传文件内容。
7. 权限控制与安全加固
7.1 通过 UserCanWrite 实现细粒度权限控制
在线编辑的权限控制必须在后端做严,不能依赖前端藏按钮。也就是在前面的CheckFileInfo里返回正确的UserCanWrite。业务系统本身的角色权限已经做好分类:只读者、编辑者、所有者。传到后端后,通过 token 里的 userId 判断出角色,分别映射到不同的UserCanWrite取值。
这里要提醒的是:只在 CheckFileInfo 里控制还不够,PutFile 接口同样要做权限校验。万一用户自己拼 URL 直接向后端发保存请求,后端必须能在 putFile 方法里校验 token 的权限。我曾经见过一个系统,因为只在 CheckFileInfo 里拦截了只读用户,结果有用户用开发工具改了 URL 里的 fileId,把别人的文件给覆盖了。这种事故在办公系统里算严重事故,必须堵牢。
7.2 文件扩展名白名单与病毒扫描
Collabora 理论上支持编辑很多格式,但不能让它无限制处理你系统里的所有文件。后端生成编辑地址时,要判断这个文件扩展名在不在白名单里。我们的白名单只有三个:docx、xlsx、pptx,外加 pdf 只读预览。其它格式一律走"下载"流程,不接入在线编辑。
文件上传时的安全做得再好,也要防一手存量文件被用户改名伪装。我在 PutFile 之后对保存的内容做一次内容类型校验,确认仍然是合法的 Office 格式,如果文件头不对就直接报错,不让它落盘。有条件的话,接入 ClamAV 之类的病毒扫描,编辑过的文件也走一边扫描再入库,这个视预算和运维能力而定。
7.3 审计日志:记录所有打开和保存动作
在线编辑场景天然适合做审计。每次 Collabora 调用 CheckFileInfo、GetFile、PutFile,后端都可以记录一条日志:谁在什么时间打开了哪个文件、是查看还是编辑、保存后的文件版本号是多少。
我这边是直接复用已有的操作日志表,在 WopiController 里插入一条异步日志记录,不阻塞正常响应流程。用持久化队列缓冲一下日志,系统压力大时也不会拖垮接口。这个投入产出比非常高,出了文档纠纷时能快速定位是谁在什么时间做了最后一次保存。
7.4 生产环境强制 HTTPS 的必要性
前面说过,开发环境可以用DONT_ENFORCE_HTTPS=true,但生产环境我是强烈建议上 HTTPS。除了安全考量,Collabora 对非 HTTPS 环境下的一些 API 行为本身就很诡异:有时能打开编辑页,但协同功能时好时坏,WebSocket 在非安全上下文里受限严重,很多前端浏览器特性直接禁用。如果公司已有统一的 HTTPS 网关,那就简单了,nginx 反代配置好证书,Collabora 的ServerName填 HTTPS 域名,extra_params里的--o:ssl.termination=true开启,所有请求路径保持一致即可。
如果你用的是内网部署、短期不打算上 HTTPS,务必保证只有内网用户能访问 Collabora 端口,不要把 9980 直接暴露到公网。Collabora 自带的管理后台没有暴力破解防护,直接暴露公网风险太大。
最后分享一个经验:在整个接入过程里,最让我意外的地方不是 WOPI 接口有多难写,而是"文件锁 + 版本判断"这两个基础概念牵扯出的问题最多。文档在线编辑不像普通 CRUD,它的状态流转非常依赖服务端与编辑器的默契配合,一点小细节没照顾到,用户体感就是"打不开文件""保存失败""一直提示刷新"。如果你也正在集成这个方案,建议先拿一个测试文件把整条链路走通,再逐步加上权限、审计和锁的细化逻辑,千万不要一上来就想一次到位。等这套基础稳定了,后面再扩展多人协同、历史版本回滚、水印这些高级能力,都会顺手很多。