尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

OnlyOffice集成Spring Boot:从Docker部署到在线编辑全流程解析

OnlyOffice集成Spring Boot:从Docker部署到在线编辑全流程解析 公司项目里的文档中心之前一直只有上传下载功能后来需求升级要求在网页里直接预览和编辑 Word、Excel、PPT还要能保留历史版本、随时回滚。我评估了几套方案之后最终选了 OnlyOffice Spring Boot 的组合OnlyOffice 负责在线编辑能力Spring Boot 处理文件存储、权限校验和业务回调。这篇就把从 Docker 部署 OnlyOffice 到后端接口设计再到前端嵌入的完整过程中整理成文部署命令、JWT 配置、回调接口、Vue3 接入方式都会讲也会把这段时间踩过的坑和排查思路一起写出来。这个项目真正要解决的问题不是“把一个编辑器嵌进网页”而是让在线编辑这条链路稳定跑通用户打开页面能编辑编辑完能自动存回自己的文件系统其他人下次打开能拿到最新版本同时数据不经过第三方云服务。OnlyOffice 是开源的可以自建服务文件内容留在内网这是它能打动我的核心原因。下面我从整体设计开始逐步拆解部署和开发里我实际用到的方案。1. 项目整体设计一条链路贯穿部署与开发1.1 为什么选 OnlyOffice而不是前端组件或第三方云服务做在线预览和编辑我见过有人直接用 JS 组件渲染 docx也有人用微软Office Online、Google Docs 这类云服务。前端组件渲染 docx 的问题是它只能做预览编辑时格式很容易错乱尤其是带复杂表格、批注、修订的文档渲染出来总是差那么点意思而且完全没有办法处理 Excel 公式拉取和 PPT 动画这类逻辑。第三方云服务集成简单但文档内容要传到外部服务器对大部分企业内部系统来说有数据合规风险同时费用也不低。OnlyOffice 是另一条路自己部署一个文档服务它提供在线编辑能力应用侧通过标准 API 接入。我列一个当时对比的表格方便你直观感受方案类型编辑能力数据是否出境部署成本主要问题纯前端组件仅预览或弱编辑否低格式兼容差Excel复杂公式容易崩第三方云文档完整编辑是按量付费内容出网合规风险高OnlyOffice 自建完整编辑否一台服务器即可需要部署维护文档服务OnlyOffice 服务端可以用 Docker 跑起来社区版已经具备完整的编辑能力能同时处理 Word、Excel、PPT 三类文档。理论上你的业务系统只要会发 HTTP 请求就能接入。它也更适合和 Spring Boot 这类后端框架配合文件由后端管理编辑器只是一个前端组件权限、版本、存储都掌握在自己手里。1.2 核心请求链路从打开文档到自动保存整套系统至少要包含三个角色浏览器前端、Spring Boot 后端、OnlyOffice Document Server。用户点击“编辑”的那一刻开始完整的请求链路是这样的用户在前端页面点开文档前端先请求 Spring Boot 的服务端接口/api/onlyoffice/config/{fileId}拿到一个编辑器配置对象。这个对象里包含文档基本信息、用户信息、回调地址等。后端在生成配置时会用 JWT 对整个配置签名防止配置被篡改也方便文档服务器校验身份。前端拿到配置后加载 OnlyOffice 提供的api.js脚本调用new DocsAPI.DocEditor(挂载节点, 配置对象)编辑器界面就在当前页面里渲染出来。此时 OnlyOffice Document Server 会根据配置中的url字段主动去后端的下载接口拉取文件内容。这一步是服务端到服务端也就是说后端下载接口的地址必须能被 OnlyOffice 服务器访问到不能用 localhost 糊弄。用户编辑完关闭文档或触发自动保存时OnlyOffice Document Server 会向后端的回调接口发送保存请求带一个downloadUrl。后端收到通知后从这个地址拉取最新文件内容覆盖写入文件存储系统并更新数据库里的版本信息返回{error:0}给文档服务器。这套链路里每个角色的职责都清晰前端只负责展示和交互Document Server 负责编辑和保存通知Spring Boot 负责文件存储和业务控制。2. 部署篇用 Docker 快速拉起 OnlyOffice Document Server2.1 一条命令启动服务参数逐项说明OnlyOffice Document Server 的部署方式官方推荐 Docker我实际跑下来也认为这是最省事的方式。镜像仓库里直接拉onlyoffice/documentserver:8.1就好指定版本号而不是 latest可以避免版本更新带来的行为变化。下面是我在生产环境使用的启动命令docker run -d \ --restartalways \ --name onlyoffice-docs \ -p 8020:80 \ -e JWT_ENABLEDtrue \ -e JWT_SECRETbKQ3mP9xTqL2nZ7vH5rW1sF8cD4gY6uE \ -e JWT_HEADERAuthorization \ -v onlyoffice_data:/var/www/onlyoffice/Data \ -v onlyoffice_logs:/var/log/onlyoffice \ -v onlyoffice_cache:/var/lib/onlyoffice \ -v onlyoffice_db:/var/lib/postgresql \ -v onlyoffice_rabbitmq:/var/lib/rabbitmq \ onlyoffice/documentserver:8.1逐个说下关键参数。-p 8020:80是把容器内部的 80 端口映射到宿主机 8020端口不一定要固定 80因为在同一台服务器上你很可能还要跑其他服务。JWT_ENABLEDtrue这个必须开否则编辑配置和回调请求都可以被伪造线上系统开了之后要配合 Spring Boot 端用同一个JWT_SECRET。这个 secret 我建议用工具随机生成一个长字符串至少 32 位不要自己拍脑袋写一个。四个挂载卷的用途分别是onlyoffice_data保存配置和证书onlyoffice_logs保存运行日志onlyoffice_cache是文件缓存onlyoffice_db是内置 PostgreSQL 数据库的数据目录onlyoffice_rabbitmq是消息队列数据。用 Docker Volume 而不是宿主机目录的好处是迁移方便备份时只需要把 volume 一起打包。如果一旦部署后发现配置有问题通过docker logs onlyoffice-docs能第一时间看到错误日志目录独立出来之后排查起来会很舒服。还有一个容易被忽略的问题容器启动之后要确认宿主机防火墙放行了 8020 端口。很多本地验证时一切正常、放到云服务器上就页面打不开的案例基本都是这个原因。2.2 部署后自检先确认哪些东西是通的部署完不要急着写代码先做三项自检先用浏览器访问http://服务器IP:8020如果看到 OnlyOffice 的首页主体说明容器已经正常起来了页面右侧右上角会出现一个 OnlyOffice 的标识。接着访问http://服务器IP:8020/healthcheck正常情况下应该返回一段 JSON 文本。这个接口是文档服务器的健康检查地址Spring Boot 后续做定时健康检测时也能用。最后测一下文档转换接口能不能通。http://服务器IP:8020/ConvertService.ashx返回的应该不是 404这里不做转换调用只需要确认服务存在即可。如果这一步都通了整个文档服务就具备开发条件了。我在自检阶段遇到过一个很刁钻的问题服务器本身能访问 8020但浏览器远程访问超时排查到最后发现是云安全组只开放了 80 端口8020 没加白名单。这类问题在本地开发时永远不会出现部署到云端一定要记得同步检查安全组和防火墙规则。2.3 文件存储选型顺手把 MinIO 接进来文件存储是这个项目里很容易被忽略的一环。如果只是做 demo把文件放在 Spring Boot 本地目录就行但接 OnlyOffice 之后你会发现一个问题文档编辑保存会产生新版本每次保存都要覆盖旧文件如果直接用本地磁盘保存管理和迁移到云存储时会很被动。我在这个项目里把文件存储接到了 MinIO。MinIO 是兼容 S3 协议的对象存储Docker 一键启动内部系统用非常合适。MinIO 和 Spring Boot 集成其实很标准在pom.xml里加依赖dependency groupIdio.minio/groupId artifactIdminio/artifactId version8.5.7/version /dependency然后写一个简单的配置类读取 MinIO endpoint、accessKey、secretKey初始化客户端。业务接口里需要下载文件时从 MinIO 拉流返回给调用方。文件结构上我用 bucket 名称区分文件类型objectName 直接用存储的文件ID文件原始文件名、扩展名、大小这些元数据存在业务数据库里。MinIO 在这里的价值是OnlyOffice 下载文件时走的是 Spring Boot 的统一下载接口接口内部到 MinIO 取流不直接暴露 MinIO 给外部也方便以后做水印、权限控制。在线编辑保存时后端也是把新文件上传到 MinIO整个过程文件不会落在临时目录里产生散落文件。3. 开发篇Spring Boot 后端接口设计与安全校验3.1 Maven 项目依赖与 OnlyOffice 配置项回到 Spring Boot 侧。在你已有的项目里加配置核心依赖就是 web 模块和一个 JWT 库。JWT 这块我用的 jjwt比较轻量Spring Boot 社区用得也多dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency然后我在application.yml里单独开了一个 onlyoffice 配置段onlyoffice: docs: server-url: http://192.168.1.20:8020 jwt-secret: bKQ3mP9xTqL2nZ7vH5rW1sF8cD4gY6uE callback-url-prefix: http://192.168.1.10:8080 storage: type: minio minio-endpoint: http://192.168.1.21:9000 minio-access-key: minioadmin minio-secret-key: minioadmin bucket: company-docs这里有一个关键点server-url和callback-url-prefix都要配置成内网可访问的地址不能填localhost。因为 OnlyOffice Document Server 运行在 Docker 容器里它访问宿主机地址时的 localhost 指向的是容器自己不是你的 Spring Boot 服务。所以要用局域网 IP 或者域名保证从容器里能访问到宿主机上这两个服务。3.2 返回编辑器配置的核心接口后端最核心的接口就是生成编辑器配置。我把它放在一个OnlyOfficeController里只暴露一个 GET 接口RestController RequestMapping(/api/onlyoffice) RequiredArgsConstructor public class OnlyOfficeController { private final FileService fileService; private final JwtUtil jwtUtil; private final OnlyOfficeProperties properties; GetMapping(/config/{fileId}) public MapString, Object getEditorConfig(PathVariable Long fileId) { // 1. 查询文件元数据 FileInfo fileInfo fileService.getById(fileId); if (fileInfo null) { throw new RuntimeException(文件不存在); } // 2. 判断文档类型OnlyOffice要求传 word/cell/slide String docType getDocType(fileInfo.getFileExt()); // 3. 构造下载url必须能让OnlyOffice服务端访问到 String downloadUrl properties.getCallBackUrlPrefix() /api/onlyoffice/download/ fileId; // 4. 构造回调url String callbackUrl properties.getCallBackUrlPrefix() /api/onlyoffice/callback; // 5. 组装编辑器配置 MapString, Object document new HashMap(); document.put(fileType, fileInfo.getFileExt()); document.put(key, fileService.buildFileKey(fileInfo)); document.put(title, fileInfo.getFileName()); document.put(url, downloadUrl); document.put(permissions, Map.of(edit, true, download, false)); MapString, Object editorConfig new HashMap(); editorConfig.put(callbackUrl, callbackUrl); editorConfig.put(lang, zh-CN); editorConfig.put(mode, edit); editorConfig.put(user, Map.of(id, 1001, name, 张三)); MapString, Object config new HashMap(); config.put(document, document); config.put(editorConfig, editorConfig); config.put(documentType, docType); // 6. 签名整个config String token jwtUtil.signConfig(config); config.put(token, token); // 7. 返回给前端docApiUrl是前端加载api.js的地址 return Map.of( docApiUrl, properties.getDocsServerUrl() /web-apps/apps/api/documents/api.js, config, config ); } private String getDocType(String ext) { return switch (ext.toLowerCase()) { case docx, doc, odt, txt - word; case xlsx, xls, ods, csv - cell; case pptx, ppt, odp - slide; default - throw new RuntimeException(不支持的文档类型); }; } }这个接口有几个细节要说明白。key字段是文档版本标识OnlyOffice 用它来判断文档是否变化过。很多踩坑博主都说过key 不能一成不变也不能每次打开都随机生成。我的做法是用文件ID 修改时间戳组装文件内容变化时 key 才变化同一文件的多个会话打开时 key 是相同的这样 OnlyOffice 能正确维持编辑状态。document.permissions.download我设置成 false这样用户在前端工具栏里看不到下载按钮文件统一走后端接口权限都在业务侧控制避免文件被随意带走。user字段传的是当前登录用户的 ID 和名称多人协同时编辑器右上角会显示这些信息。3.3 保存回调接口与 JWT 验签回调接口是整条链路里决定文件能不能保存回去的地方。OnlyOffice Document Server 在用户保存或者关闭文档的时候会向后端发一个 POST 请求。后端必须处理这个请求否则你编辑的内容全丢。回调接口完整逻辑我拆成几块。第一块是验签。启用 JWT 之后OnlyOffice 发送的回调请求头里会带Authorization: Bearer token后端收到请求要先用同一个 secret 验证验签失败直接返回错误避免伪造请求覆盖文件。PostMapping(/callback) public MapString, Object callback( RequestHeader(value authorization, required false) String authHeader, RequestBody(required false) MapString, Object body) { // 1. 验签 if (StringUtils.hasText(authHeader) authHeader.startsWith(Bearer )) { String token authHeader.substring(7); try { jwtUtil.verifyToken(token); } catch (Exception e) { log.error(OnlyOffice回调验签失败, e); return Map.of(error, 1, message, JWT verify failed); } } // 2. 处理保存逻辑 handleCallback(body); // 3. 返回成功 return Map.of(error, 0); }回调体的核心字段是status和url。status 的含义要背下来status含义后端处理建议1用户正在编辑中不处理2文档已准备好保存下载 url 指向的文件覆盖保存3文档保存出错记录日志通知用户4用户关闭编辑器文档可保存下载 url 指向的文件覆盖保存6正在编辑但保存接连失败告警提示7强制保存完成下载 url 指向的文件覆盖保存我在代码里主要处理 status 等于 2 或者 4 的情况。从 body 里取url字段这个 url 是 OnlyOffice 提供的文件下载地址用 RestTemplate 或者 OkHttp 拉取文件输入流然后转存到 MinIO再更新数据库里文件的版本号。有一个非常容易踩的坑回调接口必须返回{error:0}。如果你返回了 200 但 body 不是这个格式OnlyOffice 会认为保存失败让用户重新保存。我见过有人直接把整个业务对象返回结果编辑器一直报保存失败查了半天才发现是返回 JSON 结构不对。3.4 历史版本和批注业务侧的数据闭环OnlyOffice 本身有版本历史功能但那是在它的文档服务器内部不回传到你的业务系统。如果要让用户在业务系统里查看历史版本就得在回调保存时做版本管理我这里就碰到了搜集热词里那个“代码查看历史修改记录”的需求每次确认保存成功我就往历史版本表插一条记录存文件版本号、保存时间、操作人文件本身存到 MinIO 的不同 objectName 下。表结构大概就是文件ID、版本号、文件存储路径、保存时间、操作人ID。下次用户想查看历史版本点击某个历史版本后端就把对应版本的 MinIO 文件生成临时下载链接或者直接用 OnlyOffice 打开这一版做预览。关于批注很多人问 OnlyOffice 的批注怎么通过 API 取出来。实际上批注是保存在文档内部的docx 本质上是个 zip 包里面有个word/comments.xml文件存批注内容。你要拿到结构化数据需要在保存回调时把 docx 文件存下来然后解析 xml。如果只想在系统里展示最近批注也可以把文件解压后读取comments.xml用 SAX 或者 DOM 解析出author和comment标签里的内容批量入库。我的做法更简单粗暴保存回调时把整个 docx 文件包存一次OLAP 场景需要统计时再统一解析。这样不会影响在线编辑的速度解析是异步任务。4. 前端接入Vue3 里通过 api.js 加载在线编辑器4.1 动态加载 api.js 并挂载编辑器前端这一层我用的是 Vue3 axios。关键是你不能手动在 HTML 里硬编码写死生成脚本标签因为 Different 环境下的docApiUrl是不同的。我选择在拿到后端配置后再动态加载脚本。下面是我在组件里的完整思路先创建编辑区域再请求配置再动态加载脚本template div ideditor-container stylewidth: 100%; height: 100%;/div /template script setup import { onMounted, onBeforeUnmount } from vue import axios from axios const props defineProps({ fileId: { type: [String, Number], required: true } }) let loadedScript null onMounted(async () { const { data } await axios.get(/api/onlyoffice/config/${props.fileId}) const { docApiUrl, config } data loadedScript document.createElement(script) loadedScript.src docApiUrl loadedScript.onload () { window.DocsAPI.DocEditor(editor-container, config) } document.head.appendChild(loadedScript) }) onBeforeUnmount(() { if (loadedScript) { document.head.removeChild(loadedScript) delete window.DocsAPI } }) /script这段代码核心只有一句话new DocsAPI.DocEditor(挂载节点ID, config)。config 必须是后端签名并返回的那一整个对象不能自己在前端拼因为里面已经包含了令牌。我在这里栽过一次直接在 index.html 里静态引用了另一个环境的 api.js导致换环境部署时编辑器一直加载失败。后来统一改成动态加载并把docApiUrl放在后端返回环境差异就只在后端配置里迁移了。4.2 配置项里藏着的权限控制细节OnlyOffice 编辑器配置对象里权限控制主要在document.permissions和editorConfig.customization两块。permissions控制的是文档层面能力比如print控制打印按钮download控制下载按钮edit控制是否允许编辑。customization控制的是工具栏和外观比如autosave是否自动保存compactHeader是否折叠顶部菜单。后台管理系统一般建议把下载关掉文件统一走业务系统这样日志和权限都能管住。编辑权限则要按角色动态设置普通员工只读负责人可编辑。我在后端生成 config 时会根据当前登录人的角色决定edit的值而不是全部给 true。还有一个小细节editorConfig.lang建议设置为zh-CN你不想让用户看到满屏英文再反馈说国际化没做好。多人协作时user.id必须唯一如果两个用户用了同一个 ID编辑器会出现串名或权限错乱的情况。4.3 同时打开多份文档的注意点打开多份文档的场景显得麻烦点。我的系统里允许用户在多个标签页同时编辑不同文件除了上面说的 key 要稳定还要注意前端组件实例和脚本加载的重复问题。如果你一个页面里要挂载多个编辑器就要把编辑节点做成动态创建避免多个编辑器共用同一个 div id。最坑的是页面切走后document.head上的 api.js 脚本不会自动卸载如果下次加载又 append 一个脚本就加载了多次虽然 OnlyOffice 一般不会报错但会白白浪费请求。我在卸载组件时会手动移除 script 标签并清理window.DocsAPI确保每次都是干净环境。如果实际体验中发现编辑器刷新后白屏第一反应就用浏览器开发者工具看有没有重复的 api.js 请求。5. 常见问题与排查技巧实录5.1 编辑器一直转圈先按链路顺序查连通性编辑器一直转圈是最常见的现象。我的排查习惯是先访问http://OnlyOffice服务器IP:8020确认文档服务本身是活的。然后回到业务系统页面打开浏览器开发者工具定位 api.js 加载请求是否成功。如果 api.js 加载失败方向就锁定在跨域或网络不通排查 Nginx 代理或防火墙。如果 api.js 加载正常但编辑器一直停在加载动画问题大概率出现在文档 URL 拉取这个环节。OnlyOffice Document Server 启动时会去你配置里指定的document.url拉文件服务端到服务端的请求如果失败页面就会一直转握在加载。这时可以去docker logs onlyoffice-docs --tail 200看容器日志它会直接报出拉取文档 URL 时的错误码。有一个我印象深刻的 case后端下载接口绑定了登录拦截OnlyOffice 服务端去下载时返回 302 跳转到登录页文档自然加载不出来。排查完后我把下载接口加进了白名单只允许业务密钥访问不经过用户会话认证。5.2 JWT 报 401/403多半是密钥或 Header 不一致启用 JWT 后出现 401、403基本三件事要查后端配置的jwt-secret和 Docker 启动时的JWT_SECRET是否完全一致。多了一个空格都不行。前端拿到 config 后token字段有没有传递给 DocsAPI。如果你自己手写配置对象漏了这个字段文档服务会拒签。回调验签用的 Header 名称是否正确。我这边的配置是JWT_HEADERAuthorization回调请求会带Authorization: Bearer xxx如果你后端接口里读的是其他 Header自然验不了。还有一个更隐蔽的问题JWT_ENABLEDtrue之后文档服务器内部的一些命令请求同样要求签名如果你用旧版的集成代码或者用了官方文档之外的签名方案会莫名其妙出现 permissions 相关的 401。遇到这类问题不要猜直接看容器日志里的报错信息JWT 错误都会打印出来。5.3 文档保存不回去回调地址优先级最高编辑页面正常、文件也能打开但保存后业务系统里文件内容不更新这是让我最头疼的一类问题。检查路径很固定先看 OnlyOffice 回调有没有发出来看文档服务器日志里有没有 POST 到回调地址的记录再看回调接口返回是不是{error:0}最后确认回调地址从 OnlyOffice 容器内部能访问。关于第三条我多说一句。如果你的 Spring Boot 跑在宿主机 8080 端口回调地址千万别写http://localhost:8080。Document Server 容器内的 localhost 是它自己不是宿主机。你写https://localhost:8080它访问的是自己容器里的 8080肯定失败。改成http://192.168.x.x:8080或者服务编排里的容器名问题就没了。如果你是通过 Nginx 反向代理访问 Spring Boot回调地址要写 Nginx 对外可达的域名或局域网地址不能写内网 IP:8080否则 Nginx 转发规则对不上也会 404。我前前后后因为这种环境细节查了好几次浪费了不少时间。5.4 中文乱码和中文字体缺失OnlyOffice 容器内置字体是有限的遇到中文文档经常显示成方块或者乱码。解决办法是把中文字体包拷贝进容器然后刷新字体缓存。我在项目里常用思源黑体和宋体操作方式是docker cp simsun.ttc onlyoffice-docs:/usr/share/fonts/ docker exec onlyoffice-docs fc-cache -fv docker restart onlyoffice-docs字体文件不一定要很大常规的宋体、黑体就够覆盖大部分办公文档。如果你发现某些 PDF 导出后中文仍然发虚那就是字体集合还不够全再补几个常见中文字体就好。需要注意的是字体更新后要重启容器而且之前已经打开过的编辑器页面最好强制刷新一下不然浏览器缓存可能会让你误以为没生效。5.5 文件锁与版本冲突多人同时编辑一份文档时OnlyOffice 的协同编辑默认是实时的但如果你的后端在保存回调时动了 key就会引发版本冲突某个用户编辑器里会弹出提示“文档已被修改请重新加载”。这通常是因为我在回调里重新生成了 key把 key 和文件版本绑得太紧。后来我意识到key 的意义只是让文档服务器区分当前版本内容不是用户每次编辑后的唯一标记。如果你在保存回调后立即改 key旧会话再发起操作时就会拿新 key 去校验导致文档被强制重载。现在我的做法是 key 只由fileId 修改时间(秒级)组成文件内容真正变化时才变这样大多数冲突都规避掉了。对于需要严格一致性的场景我还会在打开文档前检查文件是否处于锁定状态毕竟有些场景不允许同时编辑同一个文件。这些细节里最值得反复确认的还是网络链路OnlyOffice 和 Spring Boot 之间的互相访问关系决定了你能不能在半小时内把整个集成跑通。很多我踩过的坑最后都归结为“回调地址写成了 localhost”“下载接口被安全拦截”“JWT 密钥不一致”这几个点上。如果按照这里的分层排查思路走遇到问题大概率能在几十分钟内定位不会像我那样在文档服务器日志和浏览器控制台之间来回折腾好几天。
返回列表