DzzOffice 里把 OnlyOffice 装上,点开一个文档,编辑器没出来,页面上直接甩一句“文档安全令牌未正确形成”,这种场面我见过不止一次。第一次遇到时我以为是 OnlyOffice 没装好,重装了两次,最后发现根本不是安装问题,而是 DzzOffice 和 Document Server 之间的令牌握手没对上。这个报错在 onlyoffice安装问题里非常典型,尤其是你已经在用 DzzOffice 做在线编辑文书、又想让 OnlyOffice 承担预览和协作编辑的时候。它解决的痛点很明确:先让文档能打开,而不是卡在权限校验门口。适合谁看?正在折腾 DzzOffice + OnlyOffice 集成的运维、后端、PHP 开发者,以及用 SpringBoot 集成 OnlyOffice 或做 onlyoffice在线编辑 的同行。下面我按临时解决办法来写,先把路走通,再谈后面怎么修正规。
1. 先搞清楚 dzzoffice 与 onlyoffice 的令牌到底卡在哪
1.1 报错现场:不是文档打不开,而是握手没通过
“文档安全令牌未正确形成”这句话一般不是 DzzOffice 自己弹的,而是 OnlyOffice Document Server 返回给前端的。你看到的页面可能还停在 DzzOffice 的文档列表,或者已经跳到编辑器容器,但编辑器区域是空白,控制台里能看到 403、500,或者一条关于 token 的报错。很多人第一反应是文档地址错了、权限不对、OnlyOffice 没启动,实际上文档可能完全正常,DzzOffice 也能读到文件,问题出在打开编辑器之前的 config 请求上。
OnlyOffice 打开文档时,浏览器会先向 Document Server 请求编辑器脚本,然后带着一个 config 去初始化。这个 config 里通常包含文档类型、文件地址、权限、用户信息、回调地址、编辑器界面语言等。如果 Document Server 开启了令牌校验,它还会要求这个 config 里带一个 JWT,或者请求头里带 Authorization。DzzOffice 负责生成这个 JWT,Document Server 负责验证。验证不过,就直接拒绝初始化,报错就是“文档安全令牌未正确形成”。
所以这个问题的本质不是“文档损坏”,也不是“OnlyOffice 安装失败”,而是两套系统在密钥、算法、字段、时间、传输链路上没有对齐。临时解决办法的目标就很清楚了:要么让 Document Server 暂时不校验,要么让 DzzOffice 生成一个能被 Document Server 认可的令牌。
1.2 令牌生成的三个关键角色:DzzOffice、Document Server、浏览器
先把链路拆开。DzzOffice 是业务入口,它知道当前用户是谁、能打开哪个文件、文件在哪个存储位置。OnlyOffice Document Server 是编辑器服务,它只认 config 和 token,不关心你在 DzzOffice 里是什么角色。浏览器夹在中间,它拿到 DzzOffice 给的页面和 config,再去请求 Document Server。
令牌校验正常时,流程大体是这样:
- 用户在 DzzOffice 点击文档。
- DzzOffice 根据当前文件、用户、权限生成一份 config。
- DzzOffice 用双方约定好的 secret,对 config 或相关 payload 做 JWT 签名。
- DzzOffice 把 config 和 token 一起交给浏览器。
- 浏览器请求 OnlyOffice Document Server。
- Document Server 用同一个 secret 验证 token。
- 验证通过后,编辑器加载文档,后续保存时再通过回调地址通知 DzzOffice。
失败通常发生在第 3 步到第 6 步之间。最常见的是 secret 不一致:DzzOffice 插件里填的是 A,OnlyOffice 的 local.json 里是 B,签名和验签用的根本不是同一把钥匙。第二种是字段不完整:JWT 只签了用户 ID,没有签 document、editorConfig、permissions 等关键字段,Document Server 认为 token 和 config 对不上。第三种是传输丢失:反向代理把 Authorization 头吃掉了,或者 token 放在 URL 里被编码坏了。第四种是时间不同步:JWT 的 exp 过期,或者 iat 比服务器时间快太多,验签直接失败。
JWT 本身是三段式:header.payload.signature。header 说明算法,常见是 HS256;payload 放声明;signature 是用 secret 算出来的签名。任何一段被改变、secret 不对、算法不匹配,都会导致“未正确形成”。这也是为什么只改一个字符都不行,密钥前后的空格、换行、引号都可能让结果完全不同。
1.3 为什么临时关闭校验能先把文档打开
临时解决办法里最快的一种,就是让 OnlyOffice Document Server 关闭令牌校验。关闭后,Document Server 不再检查 config 里的 token,也不要求 Authorization。DzzOffice 发过来的 config 只要格式正确、文件地址可达,编辑器就能打开。对于内网环境、测试环境、或者是业务急着要用的场景,这一招通常几分钟就能见效。
但必须说清楚:这是临时办法,不是长期方案。关闭令牌校验等于把 Document Server 的 config 接口暴露给任何能访问它的人。只要别人知道文件 URL,或者能构造 config,就可能绕过 DzzOffice 的权限控制。尤其是 Document Server 直接暴露在公网、又没有额外访问控制时,风险很高。我的习惯是:只在隔离内网、临时排障、或者刚刚迁移完服务时关闭校验,排障结束后马上恢复,并把 secret 重新对齐。
另外,关闭校验不代表回调地址、文件地址、跨域配置就自动正确。编辑器能打开,不等于保存一定成功。如果 DzzOffice 和 Document Server 之间的回调 URL 不通,用户编辑完点保存,还是会失败。所以临时关闭校验只是把“打开文档”这一步先放行,后面的保存链路要单独查。
2. 临时解决办法一:在 OnlyOffice 端关闭令牌校验
2.1 找到配置文件并先备份
OnlyOffice Document Server 的配置通常分两层:默认配置在default.json,本地覆盖在local.json。你直接改 default.json 也能生效,但升级或重装时容易被覆盖,所以更推荐写local.json。在常见的 Linux 安装方式下,路径是:
/etc/onlyoffice/documentserver/local.json如果这个文件不存在,可以自己创建。Docker 部署的 OnlyOffice,路径通常在容器内同样的位置,你需要先进入容器:
docker exec -it onlyoffice-documentserver bash ls -l /etc/onlyoffice/documentserver/改之前一定先备份,别嫌麻烦:
cp /etc/onlyoffice/documentserver/local.json /etc/onlyoffice/documentserver/local.json.bak如果没有 local.json,也可以先备份 default.json,虽然不推荐直接改它。备份的意义在于,临时方案失效或者要恢复校验时,你能快速回到原状态。我踩过一次坑:手快改了 default.json,后来升级 Document Server,配置被覆盖,令牌校验又开了,DzzOffice 那边却没改,结果又报同样的错。从那以后,我只在 local.json 里做覆盖。
2.2 写入关闭令牌的配置
关闭令牌校验的核心配置在services.CoAuthoring.token下面。一个常见的 local.json 写法如下:
{ "services": { "CoAuthoring": { "token": { "enabled": false, "secret": { "browser": { "string": "" }, "inbox": { "string": "" }, "outbox": { "string": "" }, "session": { "string": "" } } } } } }这里有几个细节要注意。第一,字段名是enabled,不是enable。有些旧文章写成enable,新版本不认,改完等于没改。第二,secret 的四个 string 建议一起清空,尤其是 browser。只把 enabled 改成 false、secret 还留着旧值,某些版本下仍可能出问题。第三,JSON 格式必须严格,逗号、引号、括号都不能错。改完可以用python -m json.tool或jq检查:
python -m json.tool /etc/onlyoffice/documentserver/local.json jq . /etc/onlyoffice/documentserver/local.json如果 JSON 不合法,Document Server 可能启动失败,或者继续用旧配置。我见过最隐蔽的一次,是复制配置时多了一个中文引号,肉眼看不出来,服务重启后日志里才报解析错误。
注意:关闭令牌校验只适合内网临时排障。公网环境不要长期保持关闭,否则任何能访问 Document Server 的人都有可能绕过业务系统的权限。
2.3 重启服务并确认配置真的生效
改完配置必须重启。不同安装方式命令不一样:
# 普通 Linux 安装 systemctl restart onlyoffice-documentserver # 使用 supervisor 的旧版本 supervisorctl restart all # Docker 部署 docker restart onlyoffice-documentserver重启后看服务状态和日志:
systemctl status onlyoffice-documentserver tail -f /var/log/onlyoffice/documentserver/docservice/out.log tail -f /var/log/onlyoffice/documentserver/converter/out.log如果是 Docker:
docker logs -f onlyoffice-documentserver然后访问健康检查接口:
curl http://127.0.0.1:8080/healthcheck返回true一般说明服务起来了。接下来回到 DzzOffice,强制刷新浏览器,最好用无痕窗口再试。如果 DzzOffice 有缓存,后台清一下缓存,或者删掉data/cache下的相关缓存文件。很多时候配置已经生效,但浏览器还在用旧的编辑器 iframe,看起来像没改。
如果还是报“文档安全令牌未正确形成”,先别怀疑配置没写对,去看 Document Server 日志里有没有 token 相关记录。如果日志里仍然出现 JWT 校验失败,说明你改的 local.json 没被加载,或者还有第二个 Document Server 实例在提供服务。检查 Nginx 转发到了哪个后端,检查 Docker 是否有多个容器,检查是否有负载均衡。
3. 临时解决办法二:让 DzzOffice 与 OnlyOffice 的密钥重新对齐
3.1 DzzOffice 插件里的配置项怎么填
如果你不想关闭 OnlyOffice 的令牌校验,或者关闭后仍然希望走正规链路,那就需要把 DzzOffice 这边的密钥和 OnlyOffice 对齐。DzzOffice 通常在后台“应用管理”或“应用市场”里安装 OnlyOffice 连接器,然后在插件设置里填 Document Server 地址和密钥。
典型配置项包括:
| 配置项 | 说明 | 常见错误 |
|---|---|---|
| Document Server 地址 | OnlyOffice 服务地址 | 多了或少了一个斜杠,协议写错 |
| 密钥 | 与 OnlyOffice 的 secret 一致 | 前后有空格、换行,或填了错误的值 |
| 回调地址 | DzzOffice 可被 OnlyOffice 访问的地址 | 填了 127.0.0.1,Document Server 访问不到 |
| 编辑器语言 | 界面语言 | 多语言场景下与实际用户不匹配 |
| 文件存储地址 | 文档真实地址 | 外网地址和内网地址混用 |
密钥对齐的关键是:DzzOffice 插件里填的 secret,必须和 OnlyOfficelocal.json里services.CoAuthoring.secret.browser.string完全一致。注意是完全一致,大小写、空格、换行都算。我遇到过最离谱的一次,是运维在密钥末尾多敲了一个空格,前端看不出来,JWT 签名就是不对。后来用sed -n l打印不可见字符才找到。
如果你已经决定临时关闭 OnlyOffice 的令牌校验,那么 DzzOffice 这边的密钥可以留空。但有些版本的插件如果检测到密钥为空,会拒绝生成 config,或者仍然尝试拼接 token。所以关闭校验后,最好把 DzzOffice 插件里的密钥也清掉,保存,再清缓存。两边状态要一致:OnlyOffice 不校验,DzzOffice 不强制签,这样最省事。
3.2 JWT 生成代码的常见错误与修补位置
DzzOffice 的 OnlyOffice 连接器通常是 PHP 写的,里面会调用 JWT 库生成 token。常见库是firebase/php-jwt。如果你需要临时修代码,先找到生成 config 和 token 的文件。插件目录一般在 DzzOffice 的dzz或core相关目录下,名字里可能带onlyoffice。你可以用 grep 搜:
grep -R "JWT" /path/to/dzzoffice --include="*.php" grep -R "onlyoffice" /path/to/dzzoffice --include="*.php" grep -R "secret" /path/to/dzzoffice --include="*.php"找到之后重点看几个地方:
- secret 是不是硬编码的,和 OnlyOffice 当前 secret 是否一致。
- JWT 算法是不是 HS256,OnlyOffice 是否支持。
- payload 是否包含 config 里的关键字段,还是只签了一个空的数组。
- token 是放在 config 的
token字段,还是放在请求头 Authorization 里。 - 生成 token 之前,config 是否已经被修改过,导致签名和实际内容不一致。
一个 PHP 侧的正确思路大致如下:
use Firebase\JWT\JWT; $config = [ 'document' => [ 'fileType' => 'docx', 'key' => $fileKey, 'title' => $fileName, 'url' => $fileUrl, ], 'editorConfig' => [ 'mode' => 'edit', 'lang' => 'zh-CN', 'user' => [ 'id' => (string)$userId, 'name' => $userName, ], 'callbackUrl' => $callbackUrl, ], ]; $secret = '这里填与 OnlyOffice 一致的密钥'; $token = JWT::encode($config, $secret, 'HS256'); $config['token'] = $token;这里的关键是:JWT 的 payload 要和 config 对应。不要只签一个userId,然后指望 Document Server 放行。Document Server 会检查 token 里的声明和实际 config 是否一致。你签的内容越完整,越不容易出问题。如果只是临时让文档打开,也可以把整个 config 作为 payload 签进去,再把 token 塞回 config。
另一个常见错误是算法不匹配。DzzOffice 插件用 HS256 签,OnlyOffice 配置却要求 HS512,或者反过来。一般 HS256 够用,双方一致即可。还有 secret 的处理,有的代码会先 base64_decode,有的直接用字符串。如果一边 decode 一边不 decode,签名必然对不上。临时修的时候,先确认 secret 的原始字符串是什么,不要想当然。
3.3 时间同步、URL、协议与反向代理的隐藏影响
令牌校验不只看 secret,还看时间。JWT 里通常有iat和exp。如果 DzzOffice 服务器时间比 OnlyOffice 慢太多,或者快太多,Document Server 可能认为 token 还没生效或已经过期。先查时间:
date timedatectl两台服务器最好都开启时间同步。内网环境如果没有外网时间源,至少保证两台机器时间差在几分钟以内。我遇到过一台测试机时间停在几个月前,JWT 一生成就过期,报错却只说令牌未正确形成,查了半天才发现是时间问题。
URL 和协议也很关键。DzzOffice 生成 config 时,如果填的是http://内网IP,而浏览器访问的是https://域名,Document Server 可能认为文档地址和来源不一致。反向代理场景下,要确保 Nginx 把Host、X-Forwarded-Proto、X-Forwarded-For、Authorization这些头正确传下去。一个常见的 Nginx 片段如下:
location /onlyoffice/ { proxy_pass http://127.0.0.1:8080/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Authorization $http_authorization; }如果Authorization被代理吃掉,token 就传不到 Document Server,自然报错。回调地址也必须从 Document Server 所在机器能访问到。DzzOffice 如果填127.0.0.1,Document Server 在另一台机器或容器里,回调就会失败。临时排障时,可以在 Document Server 容器里curl一下 DzzOffice 的回调地址,确认网络通。
4. 实操复盘:从报错到打开文档的完整记录
4.1 排查顺序:先看 OnlyOffice 日志,再看浏览器请求
我一般按这个顺序排查,效率最高:
- 看 OnlyOffice Document Server 日志,确认是不是 token 校验失败。
- 看浏览器 Network,确认 config 请求和响应。
- 看 DzzOffice 插件配置,确认地址和密钥。
- 看反向代理配置,确认请求头没有丢。
- 最后才去改代码或关闭校验。
日志命令:
tail -n 200 /var/log/onlyoffice/documentserver/docservice/out.log grep -i "token\|jwt\|signature\|forbidden" /var/log/onlyoffice/documentserver/docservice/out.log如果看到invalid signature、jwt malformed、token is not correctly formed,基本就是密钥或 payload 问题。如果看到connect ECONNREFUSED,那是回调地址或文件地址网络不通,不是令牌本身的问题。
4.2 浏览器 Network 面板里应该看到什么
打开浏览器开发者工具,切到 Network,勾选 Preserve log,然后点开文档。重点看几个请求:
- 请求 Document Server 的
api.js是否 200。 - 请求 config 的接口是否 200,响应里有没有
token字段。 - 请求编辑器初始化接口是否 403 或 500。
- 响应体里有没有“文档安全令牌未正确形成”的原文。
如果 config 接口返回 200,但编辑器接口 403,大概率是 token 没带过去或者签名不对。如果 config 接口就报错,说明 DzzOffice 自己生成 config 时已经出问题。可以复制请求的 token,到 JWT 调试工具里解码,看看 payload 是否完整、exp 是否过期。注意不要在生产环境随便把 token 发到第三方网站,本地解码就行。
4.3 最终配置示例与重启清单
临时关闭校验时,我的最终配置通常保持这样:
{ "services": { "CoAuthoring": { "token": { "enabled": false, "secret": { "browser": { "string": "" }, "inbox": { "string": "" }, "outbox": { "string": "" }, "session": { "string": "" } } } } } }DzzOffice 插件里 Document Server 地址填实际可访问地址,密钥留空,保存后清缓存。重启清单:
systemctl restart onlyoffice-documentserver systemctl restart nginx # 如果有 PHP-FPM,也重启一下 systemctl restart php-fpm然后无痕窗口测试。如果打开成功,先别急着庆祝,继续测试编辑、保存、多人协作。保存成功才说明回调链路也通。
5. 常见问题速查与避坑清单
5.1 常见问题速查表
| 现象 | 可能原因 | 临时处理 |
|---|---|---|
| 报错“文档安全令牌未正确形成” | OnlyOffice 开了校验,DzzOffice 没签或签错 | 关闭 OnlyOffice token 校验,或对齐 secret |
| 改 local.json 后仍报错 | 配置未加载、JSON 格式错、多个实例 | 检查日志,确认 local.json 生效,重启全部实例 |
| 编辑器白屏,无报错 | api.js 地址不对、跨域、端口不通 | 浏览器 Network 看 api.js 是否 200 |
| 能打开不能保存 | 回调地址不可达、权限不足 | 从 Document Server 访问回调 URL |
| 只有部分用户报错 | 用户信息、时间、key 冲突 | 检查用户 ID 和文件 key 是否唯一 |
| 重启后短暂正常又报错 | 缓存、负载均衡、配置回滚 | 清缓存,检查多节点配置一致性 |
| 多语言界面不对 | editorConfig.lang 未设置 | 在 DzzOffice 插件里指定语言 |
| Docker 部署改配置无效 | 改在宿主机,没进容器 | 进容器改,或挂载配置文件 |
5.2 避免下次再踩的几条经验
第一,改配置前先备份,改完先验证 JSON。第二,secret 不要用肉眼比对,复制粘贴后检查首尾空格。第三,内网临时关闭校验后,一定要记录在案,设定恢复时间。第四,DzzOffice 和 OnlyOffice 的地址尽量统一用域名,不要一会儿内网 IP、一会儿外网域名。第五,升级 OnlyOffice 或 DzzOffice 插件前,先确认令牌配置是否会被覆盖。第六,测试时用无痕窗口,避免浏览器缓存骗你。
还有一个很实用的经验:如果你在用 SpringBoot 集成 OnlyOffice,或者 Java 进行 onlyoffice在线编辑文书,令牌生成逻辑最好抽成独立工具类,secret 放配置中心,不要散落在业务代码里。这样 DzzOffice 那边出问题时,你可以快速比对两边的签名参数。onlyoffice多语言 场景下,lang 字段也要放进 config,别只改前端界面,否则用户看到中英混杂。
6. 如果必须保留令牌校验,后续可以怎么修
6.1 PHP 侧正确签发 JWT 的思路
长期方案一定是开启令牌校验,并且让 DzzOffice 正确签发 JWT。核心原则:签名 payload 必须覆盖 Document Server 需要校验的 config 字段,secret 双方一致,算法一致,时间有效。PHP 侧可以用firebase/php-jwt,先生成完整 config,再编码,再把 token 塞回 config。不要先塞 token 再签名,否则签名内容里包含 token 自身,容易死循环。
临时排障时,可以先在 DzzOffice 里写一个测试脚本,生成 token 后用 curl 请求 Document Server,看返回什么。这样能把浏览器、前端缓存这些干扰因素排除掉。
6.2 Java SpringBoot 集成时的注意点
Java 侧常用java-jwt或jjwt。生成逻辑类似:
Algorithm algorithm = Algorithm.HMAC256(secret); String token = JWT.create() .withIssuedAt(new Date()) .withExpiresAt(new Date(System.currentTimeMillis() + 3600_000)) .withClaim("document", documentMap) .withClaim("editorConfig", editorConfigMap) .sign(algorithm);注意 claim 的类型和 JSON 序列化结果要和 config 一致。Java 里 Map 转 JSON 后可能多出 null 字段,或者数字变成字符串,这些都可能影响校验。开发版连接器调试时,最好把生成的 token 和 config 一起打印到日志,但不要打印 secret。上线前把日志级别调回去,避免敏感信息泄露。
6.3 临时方案转长期方案的收尾动作
临时关闭校验后,文档能打开,业务先恢复,接下来要做三件事:第一,恢复 OnlyOffice 令牌校验,设置一个强 secret;第二,DzzOffice 插件里填入同样的 secret,清理缓存;第三,用普通用户、管理员、不同文件类型各测一遍打开、编辑、保存、协作。确认无误后,把 local.json 备份更新,记录变更时间和操作人。
如果 Document Server 只在内部网络使用,也建议在 Nginx 层加访问控制,不要让它直接裸奔。令牌校验是应用层的一道门,网络层再加一道,心里更踏实。
我个人在实际操作中的体会是,遇到“文档安全令牌未正确形成”先别急着怀疑人生,九成问题都在 secret、payload、时间、代理这四件事上。临时关闭校验能救急,但救完急一定要回头把令牌链路修好。最后再分享一个小技巧:把 DzzOffice 和 OnlyOffice 的配置各截一张图,改完后对照检查,比在脑子里记参数靠谱得多。