拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DzzOffice 集成 OnlyOffice:JWT令牌报错与密钥对齐

DzzOffice 集成 OnlyOffice:JWT令牌报错与密钥对齐

装过 DzzOffice 又挂了 OnlyOffice 的人,大概率都见过这一行红字:文档安全令牌未正确形成。它通常出现在编辑器 iframe 的正文区域,底下还跟一句"请联系文档服务器管理员"。第一次遇到的人往往会往网络、往端口、往跨域上想,折腾半天防火墙和白名单,结果跟这些一点关系都没有。这个报错的本质非常单纯——OnlyOffice Document Server 在打开文档时,要对浏览器传来的配置做一次 JWT 验签,签不出来、或者签的名字对不上,它就直接把编辑器毙掉,只留这一句话给你。

DzzOffice 这边负责生成编辑器配置并把它塞进页面,OnlyOffice 那边负责验。两边的密钥只要有一个字节不一样,或者 DzzOffice 这个版本压根就没往配置里塞令牌,红字就必然出现。所以所谓"临时解决办法",其实就两条路:要么把 Document Server 的校验关掉,要么把两边的密钥对齐。前者快、粗暴、五分钟能见效;后者体面、安全、但要求你搞清版本和配置层级。我下面会把这套东西从"报错出自谁"一路讲到"改完还报错怎么办",最后再说说什么情况下这两招都该退休。

1. 这行红字是 OnlyOffice 在报,不是 DzzOffice 在报

很多人第一反应是去 DzzOffice 的应用日志里翻,翻半天翻不到,因为这条错误根本没走到 DzzOffice 的后端逻辑。搞清楚"谁在说话",能省下至少一半的排查时间。

1.1 一次打开文档背后其实跑了两段链路

你在 DzzOffice 里点开一个 docx,发生的事情大致是这样:DzzOffice 后端拼出一份 JSON 配置(包含文档地址、回调地址、权限、编辑器尺寸等等),把这份 JSON 渲染进页面;浏览器加载 OnlyOffice 的 api.js,拿着这份配置去请求 Document Server;Document Server 拿到配置后先做一次校验,确认这份配置是"可信来源"发来的,然后才去把文档文件拉过来、渲染成编辑器。

关键就在"确认可信来源"这一步。OnlyOffice 用的手段是 JWT:DzzOffice 用约定的密钥,把整份配置当成 payload 签一个 HS256 的令牌,塞在配置对象里一起发过去;Document Server 用同样的密钥重新算一遍签名,对得上才放行。对不上、或者压根没令牌,它返回的就是那句"文档安全令牌未正确形成"。所以这行字是 Document Server 吐出来的,DzzOffice 只是个传话的看客,你去 DzzOffice 日志里找是找不到的。

1.2 JWT 校验开关在 OnlyOffice 里分了三个位置

不少人以为令牌校验就是一个总开关,实际上 OnlyOffice 把它拆成了三块,分别管不同方向的请求。这是我踩过坑才记住的:

配置位置管的是什么关掉后的影响
token.enable.browser浏览器侧提交的编辑器配置关掉后打开文档不再要求令牌,即你遇到的这个报错
token.enable.request.inboxDocument Server 往回调地址发请求时带令牌关掉后你的回调接口不再收到 Authorization 头
token.enable.request.outbox部分内部转发请求的令牌一般集成场景下影响较小

如果你只是想让文档能打开,改的是第一项。很多人改了request.inbox发现没用,就是位置找错了。而真正容易被忽略的是后两项——一旦你把浏览器侧校验关了,回调侧还开着,文档能打开、能编辑,但保存时静默失败,你会看到"文档已保存"的提示,刷新之后内容还是旧的。这种问题比打不开更折磨人,因为它的异常表现是"看起来正常"。

所以动手之前先想清楚:你只是要临时打开看看,还是准备长期用。临时打开,三块一起关;长期用,老老实实对齐密钥。

2. 五分钟自查:先确认是"令牌没带"还是"带了但签错"

同一个报错,根因可能完全不同。前者是 DzzOffice 没塞令牌,后者是两边密钥不一致。这两件事的处理方式相反——前者你得关校验或者升级插件,后者你只要把密钥抄对就行。所以别急着改配置,先确认是哪一种。

2.1 进 Document Server 看真实的开关状态

OnlyOffice 的配置是分层的,default.json是出厂默认,local.json是本地覆盖,运行时以合并后的结果为准。你直接看default.json会看到一大堆字段,但那不一定是生效值。最靠谱的方式是先扫一眼默认结构,确认你这个版本里字段名叫什么:

grep -n -A 25 '"token"' /etc/onlyoffice/documentserver/default.json | head -60

这一步的意义在于:不同大版本的字段命名有差异。早期版本就是inbox、outbox、session三段密钥,新一点的版本里你能看到和browser相关的键。你要照着你自己机器上吐出来的结构去写local.json,而不是照抄网上某篇三年前的文章。抄错层级的后果是配置文件语法没错、服务也能起来,但那个开关根本没生效,你会以为"改了没用",其实是没改到点上。

然后再看本地覆盖层:

cat /etc/onlyoffice/documentserver/local.json

如果这个文件里没有token这一段,说明你在用出厂默认。而很多新版本安装完之后,默认就是开启校验并且自动生成了一串随机密钥——这时候你只要拿到那串密钥填到 DzzOffice 里就行,根本不用关任何东西。密钥在哪:还是在local.json里,services.CoAuthoring.token.secret下面那几个string。

注意:如果你是用容器起 Document Server,local.json在容器内部,路径是/etc/onlyoffice/documentserver/local.json,宿主机上直接cat是看不到的。

2.2 抓一次配置请求,看 token 字段到底存不存在

打开 DzzOffice 里的文档页面,按 F12 打开开发者工具,切到 Network,过滤CommandService或者直接过滤docservice,刷新页面。你会看到一条发往 Document Server 的 POST 请求,请求体就是那份编辑器配置。

在请求体里搜token。三种结果对应三种病:

  • 完全没有 token 字段:说明 DzzOffice 这个插件版本不支持 JWT,或者你没在插件设置里填密钥。这种情况你只能去 Document Server 侧关校验,没有别的临时办法。
  • 有 token,但形如一个很短的普通字符串:那不是合法 JWT。合法 JWT 一定是三段、用点分隔、第一段以eyJ开头。
  • 有 token,是标准三段结构:那大概率是密钥不一致,或者密钥里有隐形字符。

顺便看一眼这个请求返回的内容。如果返回体里带error和-4之类的错误码,基本就坐实了是令牌问题,不用再往别处想。

2.3 用一段 Python 验签,把"猜"变成"确定"

密钥不一致这件事,靠肉眼比对是不靠谱的,尤其是密钥里有容易混淆的字符时。最省事的办法是把抓到的 token 拿去本地验一遍:

import base64, json, hmac, hashlib token = "把这里换成你抓到的 token" secret = b"把这里换成你 local.json 里的密钥" head, payload, sig = token.split(".") def b64d(s): return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4)) print(json.loads(b64d(head))) print(json.loads(b64d(payload))) want = base64.urlsafe_b64encode( hmac.new(secret, f"{head}.{payload}".encode(), hashlib.sha256).digest() ).rstrip(b"=").decode() print("签名是否匹配:", want == sig)

跑出来如果是False,密钥不一致,实锤。同时打印出来的 payload 里你能看到iat和exp两个时间戳,顺手核对一下——如果exp比当前时间早很多,说明不是密钥问题,是服务器时钟跑偏了,那是另一个坑,第 5 节会讲。

如果 payload 能解出来但结构很怪(比如里面塞的不是配置对象而是一个url字符串),那说明 DzzOffice 用的是旧版签名格式,和你这个版本的 Document Server 对不上。这种情况关校验是最快的路。

3. 止血方案一:Document Server 侧关掉令牌校验

这条路就是把门锁拆了。五分钟能搞定,代价是任何能访问到你 Document Server 的人,都可以构造一份配置去拉文档。内网自用、临时验证、演示环境,问题不大;放到公网,别这么干。

3.1 改 local.json 的最小改动集

原则是:只动local.json,永远别动default.json。后者会在升级时被覆盖,而且体量巨大,改错一格你都很难发现。

一个最小可用的覆盖内容长这样:

{ "services": { "CoAuthoring": { "token": { "enable": { "browser": false, "request": { "inbox": false, "outbox": false } }, "secret": { "inbox": { "string": "换成同一串密钥" }, "outbox": { "string": "换成同一串密钥" }, "session": { "string": "换成同一串密钥" } } } } } }

这里有个细节值得说清楚:我明明把校验关了,为什么还留着secret段?因为在你将来切回"对齐密钥"这条路的时候,密钥得有一份明确的、你能看到的来源。如果它一直是安装时随机生成的那串,你换个环境就再也找不回来了。所以我的习惯是,第一次配置的时候就把密钥固定成自己指定的一串,后面所有环境都统一用它。

改之前先备份:

cp /etc/onlyoffice/documentserver/local.json /etc/onlyoffice/documentserver/local.json.bak

改完用python3 -m json.tool过一遍,确认语法没问题。JSON 一个多余逗号就能让服务起不来,而且报错信息藏在日志里,很容易漏。

3.2 重启姿势不对等于没改

改完文件不重启,一切照旧,这是最常见的"改了没用"。但重启的方式要看你的部署形态:

原生安装(apt/rpm 装的那种):一般由 supervisor 托管,最稳的一刀是

sudo supervisorctl restart all

只重启 nginx 是没用的,配置是 docservice 进程读的。

容器部署:

docker exec -it <容器名> supervisorctl restart all

或者直接docker restart <容器名>。前者快,后者更彻底。

重启之后别急着开文档,先看日志确认配置被读进去了:

tail -f /var/log/onlyoffice/documentserver/docservice/out.log

回到 DzzOffice 刷新页面,如果这次能进编辑器,说明生效了。日志里如果还有令牌相关的告警,那说明你的改动没落到实际生效的层级上,回去核对default.json里的字段结构。

3.3 容器部署的隐藏陷阱:环境变量会把你的改动覆盖回去

这个坑我踩得很结实,值得单独拎出来说。

如果你是用docker run或者 docker-compose 起的 Document Server,很多镜像版本在容器启动时会根据环境变量重写local.json。也就是说,你docker exec进去,改好了文件,supervisorctl restart all,一切正常。然后某天你docker restart了一下容器,配置全回来了,报错也回来了,你会一脸茫然地以为是谁动了你的机器。

根源在启动脚本:镜像里有JWT_ENABLED、JWT_SECRET、JWT_HEADER这几个环境变量,启动时会把它们写进配置文件。所以正确做法是从一开始就在 compose 或 run 命令里定好,例如:

environment: - JWT_ENABLED=false - JWT_SECRET=你固定的那串密钥

改完docker compose up -d重建容器,而不是进去手动改文件。同理,想长期用对齐密钥的方案,就把JWT_ENABLED=true和JWT_SECRET一起定死,两边抄同一串。

还有一点:少数版本会缓存一份合并后的配置,重启后需要额外跑一次清理脚本才彻底生效。包里如果带了documentserver-flush-cache之类的命令,可以顺手执行一次;没有的话,docker restart一般也够。

4. 止血方案二:两边密钥对齐,比关校验体面

如果 DzzOffice 的插件版本支持 JWT(也就是设置界面里有密钥输入框),那这条路才是正解。它不牺牲安全性,也不会在升级后突然失效。麻烦点在于,密钥这东西的"对齐"比想象中脆弱。

4.1 DzzOffice 里密钥填在哪,填什么

进 DzzOffice 后台,应用管理里找到 OnlyOffice 那个应用,点它的设置。一般会有两个关键字段:文档服务器地址和密钥。地址填到 Document Server 的根路径(不要带/web-apps之类的后缀),密钥填你local.json里secret那几段的string值。

填完保存,回到文档页面刷新。这里有个很反直觉的现象:有些版本的插件改完设置后需要清一次缓存才生效,因为在应用设置保存后,前端拿到的还是旧配置。稳妥的做法是保存后退出登录再重新登录,或者干脆清一下浏览器缓存。

如果插件的设置里压根没有密钥这一栏,那说明这个版本不支持 JWT。这种情况下别硬找,直接把 Document Server 侧校验关掉更省事,硬凑只会浪费时间。

4.2 密钥里的隐形字符:空格、引号、换行

密钥不一致的案例里,我遇到过的真实原因按出现频率排下来是这样的:

  1. 复制时带上了首尾空格。local.json里的值是"string": "abc123",你从终端里cat出来复制的时候,很容易把": "之后的那一点空白也带进去。
  2. 密钥用了带引号的形式。有人图省事把密钥写成"\"abc123\"",多出来的转义引号会被算进签名。
  3. 末尾换行。从文件里复制粘贴很容易带上一个不可见的换行符,前端输入框里看不出来,但参与签名时就是一个字节的差异。
  4. 大小写被输入法改写。这个不常见但真发生过,尤其是密钥里有l、I、O、0这类字符的时候。

规避方法很简单:密钥只用纯字母和数字,长度控制在 32 位左右,别放特殊符号,别放中文。既好复制又不容易出错。生成方式随便,openssl rand -hex 16出来的 32 位十六进制串就挺好用。

填进去之后,用第 2 节的验签脚本再验一次,确认True了再往下走。这一步花两分钟,能省掉后面半小时的反复。

4.3 反向代理下密钥没变但校验失败的几种情况

密钥明明一样,还是报令牌错误,那问题就转移到"请求在中间被改了"。如果你在 Document Server 前面挂了 nginx 做反代,重点查这几件事:

  • 请求体被截断或改写。有些代理配置会对 body 做缓冲或者重写,配置 JSON 一旦被改动,签名自然对不上。检查proxy_request_buffering和client_max_body_size相关设置。
  • Header 被过滤。回调方向靠Authorization: Bearer xxx传令牌,如果代理把这个头丢了,回调就会失败。虽然它不直接导致你看到的那行红字,但会造成"能打开、不能保存"。
  • HTTPS 与 HTTP 混用。DzzOffice 走 https,而配置里回调地址是 http,浏览器会直接拦掉,表现为编辑器加载不出来。反代上补X-Forwarded-Proto头通常能解决。

这几条不一定会触发"安全令牌未正确形成",但它们和令牌问题是同一批人在同一个环境里高频遇到的,一起排查能少走弯路。

5. 改完还报错:几个高频连带问题

配置改对了、服务重启了、密钥也验过了,刷新页面还是那行红字。这种时候别怀疑人生,多半是缓存或者环境层面的问题。这一节列的几条,都是我真实遇到过的。

5.1 缓存三层:浏览器、代理、应用设置

Document Server 前端有一套自己的静态资源和接口缓存。改了配置之后,浏览器里那份 api.js 和编辑器页面可能还是旧的。最直接的办法是强制刷新(Ctrl+F5 或者无痕窗口打开),先排除浏览器这一层。

第二层是 Document Server 前面的 nginx。它对部分接口是有缓存的,重启服务能清掉;如果是独立的反代,可能需要nginx -s reload。

第三层是 DzzOffice 应用自身的配置缓存。有些插件会把文档服务器地址和密钥缓存到本地文件或数据库里,后台保存了但运行态没更新。这种情况通常"退出重新登录"或"清理站点缓存"就能解决。三层都过一遍,很多"改了没用"的悬案就破了。

5.2 时间不同步与多实例密钥不一致

JWT 里带iat和exp,签发时间和校验时间之间的偏差如果太大,验签会失败。服务器时间跑偏这种情况在闲置很久的测试机上特别常见。一条命令确认:

date -u

和标准时间对一下,差得离谱就先同步时间再试。这个坑的迷惑性在于:它的报错文案和有令牌错误时一模一样,你根本想不到是时钟的问题。

另一个场景是多实例部署。如果你的 Document Server 前面挂了负载均衡,后端有两台以上,而只有其中一台改了密钥,那么请求打到哪台就决定了成功还是失败,表现为时好时坏、刷新几次就能打开。这种"随机成功"的现象非常有辨识度,看到它基本就能锁定是多实例配置不一致。解决办法是把密钥统一写进镜像或者配置中心,而不是手动一台台改。

5.3 HTTPS 混合内容与回调地址不通

"安全令牌未正确形成"本身跟 HTTPS 无关,但在排查过程中很容易被一个相关现象带偏:编辑器加载出来了,红字没了,可文档内容区域一片空白,或者一直转圈。这通常是 Document Server 拉不到文件——回调地址或文件地址填成了外网访问不通的地址。

判断方法很直接:直接在浏览器里打开配置里的url字段那个地址(就是你那份文档文件的直链),看能不能下载下来。如果浏览器能下、Document Server 下不了,那问题在 Document Server 到 DzzOffice 这条网络链路上,而不是令牌。

顺手记一条经验:DzzOffice 后台填的文档服务器地址,必须是浏览器能访问到的地址,而不是127.0.0.1或者容器内部 IP。有人填了内网 IP,自己电脑在公司内网能打开,一回家就全是问题。地址这块建议统一用域名。

6. 什么时候必须把"临时"改成"正式"

"临时解决办法"这五个字是有重量的。它意味着你现在做的事能解决问题,但会留下一个需要还的账。什么时候必须还,我给几个判断标准。

6.1 关掉校验之后,文档服务器等于不设防

把token.enable.browser关掉之后,Document Server 对任何人发来的编辑器配置都照单全收。这句话翻译成人话是:只要能访问到你 Document Server 的地址,别人就能构造一份配置,让服务器去拉任意一个它能访问到的文件。注意这里的关键是"它能访问到的",也就是服务器所在网络里的资源,包括内网的其他服务。

内网隔离、只给自己用、当天用完就删的环境,可以接受。一旦满足下面任意一条,就该老老实实去对齐密钥:

  • Document Server 暴露在公网或者办公网
  • 有多个用户在共用
  • 你这个环境会长期存在
  • 服务器能访问到除文档目录之外的其他内部资源

6.2 让配置在升级和重建后依然存活

无论你选哪条路,配置都得有"抗升级"和"抗重建"的能力。我给自己定的规矩是三条:

一是密钥固定化。第一次部署就指定一串自己生成的密钥,写进部署脚本或者 compose 文件,而不是用安装时随机生成的。随机密钥的问题是它只存在于那一台机器的那个文件里,机器一重装,你就再也找不回来了。

二是配置来源单一。原生安装用local.json,容器部署用环境变量,二选一,不要两处都改。两处都改的结果是某次重启之后你分不清生效的是哪个,排错成本翻倍。

三是升级后必查。OnlyOffice 大版本升级会调整配置结构,字段位置可能变。升级完先按第 1 节的方法重新确认一遍字段名,再看校验开关的状态,别默认它还跟以前一样。

6.3 一个我自己在用的排错顺序

最后把我实际用的排查顺序写下来,下次再遇到这行红字,照着走一遍就行:

  1. 先看 Document Server 日志docservice/out.log,确认报错确实来自令牌校验,而不是别的错误被这行文案盖住了。
  2. 抓一次打开文档的请求,看配置里有没有token字段,是不是合法三段 JWT。
  3. 有 token 就验签,True就往下查时间同步和缓存,False就去核密钥。
  4. 没 token 就看 DzzOffice 插件有没有密钥设置栏,有就填、没有就关校验。
  5. 关校验的话,browser和request.inbox/outbox一起关,别只关一半,否则会出现能开不能存的怪毛病。
  6. 重启对应进程,容器部署记得先确认环境变量有没有把改动盖掉。

这套流程走下来,我遇到过的同类问题基本都能定位到具体一层,很少有需要把整个环境推倒重来的情况。真正让我浪费时间的,从来不是配置本身,而是"以为改了其实没生效"和"以为生效了其实改错了层"这两件事。把这两件事按住,剩下的都是体力活。

返回列表