精读《WOPI协议》

2,702 阅读10分钟

由于业务需要接入公司已有Office平台实现在线查看和编辑Office文档,经调研使用WOPI协议支持此功能。WOPI是微软基于REST API的协议,定义了一组Http操作,使客户端能够访问和改变服务器存储的文件。

本文学习下协议是如何设计的,从中学习如何提供跨应用开放协议的经验

为什么要设计这样协议

假设开发者在Host机器上部署某个业务Web服务,某天产品提到要在这个Web服务上展示和编辑Excel文件,这个需求解决方案目前主要有两种:

  • 利用Javascript SDK,以纯前端方案打开该Excel文件,这类库包括LuckySheet、SpreadJS等
  • 集成已有的在线office平台,比如微软提供了Office Online App Server平台,允许第三方集成业务直接在网页中以Iframe方式嵌入Office页面,Office页面内部打开指定的Excel文档,展示效果如下图。 集成效果图

第二种就是本文要讨论内容依赖的前提,它的好处是对前端开发者而言集成成本低,只需要通过Iframe嵌入到Host页面即可。那Office页面是如何知道去哪里打开获取到文档内容,文档信息是怎么告知给在线Office平台?这就是WOPI协议解决的问题。

WOPI协议约定了Office Online服务和集成业务侧之间的通信协议,协议约定了一组接口操作,指明怎么从集成业务方获取和改变文件,该操作基于REST协议,这样对集成方而言只要提供了这些接口实现即可,开发成本可控。

有了该协议,office页面可以嵌入到各个三方业务集成方的页面里;和一般平台型集成方案相比,集成的方向是相反的,平台型方案是希望三方业务在平台上沉淀能力,用户操作都在平台上进行, 这样用户离不开平台;而本文提到的业务集成是把office搬到业务里,用户仍然在自己业务平台上操作。

image.png

使用方式介绍

以官方文档提供的如下流程图来说明,这个图把WOPI交互流程解释比较清楚,它包括Browser、Server、Client三种角色:

流程图

  • Server:需要使用Office能力的集成方,提供office源文件,协议文档中经常提到的Host和Server概念是类似。
  • Client:简单理解为提供Office查看和编辑能力的平台,比如MS Office、WPS、石墨、Onlyoffice等

上图中,Server提供入口页面供Browser访问,Browser告诉Server需要打开的文件File,然后Server内部开启和Client之间交互,通过一系列内部流转后,用户最终看到Office打开File内容。所以明确下Server和Client含义对接下来理解协议机制非常关键。

使用上,Client一般实现了三个接口,Server开发者无需关心具体实现,只要知道每个接口大致含义即可:

  • /hosting/discovery:告诉Server,Client能支持打开的Office文档类型和打开方式,比如支持打开xlsx、docx、pptx等类型。返回内容理解为一份说明书(以XML格式展示,如下图),说明每个文档的打开方式,以链接形式存在,即图中的urlsrc,urlsrc中query包括一些模板参数,模板参数的含义见这里,Server收到后需要替换这些参数为具体值,并返回给Browser

image.png

  • /hosting/capabilities:Client对外公开的属性信息
  • /hosting/wopi/:documentType/:mode?WOPISrc=http://serverHost/wopi/files/:fileId:根据指定文档fileId,返回用户可以查看或者编辑文档的HTML内容,渲染器最终渲染该页面,域名和路径就是/hosting/discovery接口返回的urlsrc,其中
    • WOPISrc:协议约定的一个URL,需要Server提供,Browser根据该信息通知Client可以对Office文档执行WOPI规定的文档操作
    • documentType表示文档类型,包括xlsx、docx等
    • mode表示查看或者编辑模式,包括show、edit。上图中step3通过该接口访问Office Client服务

Server端的实现基本上按照协议规范实施,具体参考demo,核心包括:

  • 提供一个页面,作为浏览器访问入口,页面包括待打开文档的信息
  • http://server/wopi/files/:id: 和文档基本信息有关,操作包含
    • CheckFileInfo
    • Lock/UnLock
  • http://server/wopi/files/:id/contents:和文档内容有关,包含
    • GetFile:获取文档内容
    • PutFile:保存文档

协议核心理解

协议内容比较多,不过核心流程分解如下

认证阶段

该阶段在上述流程图中的step1、2步骤,需要Server事先提供Host页面,用户通过Browser请求Host页面(用户感知到的只有Host页面,而不是Office页面),Server内部生成两个重要信息:

  • 文档唯一ID:fileId
  • 认证信息:access_token、access_token_ttl 然后内部通过调用/hosting/discovery接口,返回一个可以访问Office服务的urlsrc。具体交互时序图如下:

image.png

第一次看到上述流程图有个疑问,为何Browser不能跳过头两步,直接进入Step3通过URL访问Office页面,这样Office客户端根据WOPISrc地址去Server执行file操作?理论上这样交互是可行,但是这么做相当于Server文档对任何外部请求都是可访问的,这对Server方是不能接受的,除非Server方信任并知道Client的地址情况下通过IP白名单机制来限制,但这样导致Server和Client存在耦合关系。

步骤1、2就是从Server获取文档权限信息,这样保护文件安全。Server在页面初始化时生成access_token,access_token看成是一个认证凭证,代表用户是否有权访问文档:

  • 生成好的access_token返回给Browser,也就是上图step2返回内容之一
  • 然后接下来Browser向Client发出的请求带上access_token,即step3发出的请求
  • Client收到后在后续和Server交互时,会原样在请求URL参数里带上access_token,这样Server会先校验请求携带的access_token是否合法,如果是说明Office有权限打开文档

这个过程可以总结为Client和Server通过Browser这个中介来传递access_token。当然access_token值并不是一直有效,WOPI协议规定Server设置access_token_ttl,用来通知Client关于access_token值的有效期,默认是10h,超过这个时间后Client会取消本次会话。

注意:step3中access_token是怎么传递给Server?如果开始时直接在URL参数里携带,URL在公网传输,就有提前泄漏access_token的风险,也就是说Browser不能主动在URL里写入access_token,但是Client是可以的,因为access_token有效性是由Server决定,Client只管在请求里原样返回并由Server校验。所以WOPI协议的demo建议step3通过POST方法访问Office平台,access_token通过表单提交,这样在https传输协议下access_token传输至少是安全的。

文档打开和编辑保存

从文档生命周期来看,文档操作包括打开、查看/编辑和保存流程。

查看和编辑是Office平台能力,打开文档请求Server获取文档内容,编辑文档后关闭页面,通知Server保存文档最新内容。WOPI协议定义了文件操作接口,其中CheckFileInfo、GetFile、PutFile接口实现上述打开和保存文件功能, GetFile、PutFile比较好理解,CheckFileInfo接口功能比较复杂,但是它决定Client端对文档的UI展示行为和后续允许的操作,这是由Server端提供属性信息决定,该接口返回包括:

  • 文件基本信息:比如大小、文件展示名字等,
  • 用户权限属性,常见属性包括:
    • UserCanWrite:指示当前请求用户是否有权限更改文件,这决定Client后续是否允许调用PutFile接口
    • ReadOnly:文档是否只读
    • UserCanRename:文档是否允许重命名,为false时Client在UI展示时不会提供重命名按钮
  • 指示Client,Server支持哪些功能属性(capabilities properties),列举几个常见属性
    • SupportsGetLock:为true说明Server支持GetLock操作
    • SupportsLocks:为true说明Server端支持Lock、Unlock等操作,Lock相关语义方面会提到
    • SupportsRename:为true说明Server端支持对文档重命名操作
    • SupportsUpdate:为true,说明Server支持更新文档操作 Server通知Client后,Client就可以在Server提供的属性决定后续操作是否允许调用,比如假设Server端通过checkFileInfo接口设置SupportsUpdate为false,那Client端知道Server不支持提供PutFile接口来更新文档,当文档关闭后Client不会把文档更新内容通知Server保存。

官方列举的属性内容比较多,有些属性之间语义有重合,所以使用者需要注意。另外有的读者会疑惑这些权限属性和access_token有什么关系?,事实上,access_token充当对任何请求来源的认证机制,只有匹配Server端生成的值才是合法的,如果Server校验access_token不合法时,Client发出的任何操作请求被拒绝;而上述权限属性是在请求被认证通过的基础上,用来限制用户在Client端的操作能力。

锁机制

如果用户有权限编辑文档内容,Server就要关心是否支持对文档的加解锁操作。锁的作用就像一把进入文档编辑的钥匙,只有拿到钥匙的合法用户才能编辑文档。锁机制判断文档是否允许多个用户同时编辑。

锁信息用锁id(lockId)标识,lockId的生命周期由Client负责控制,Server存储指定文档的lockId,在Client获取文档内容之前开始加锁,调用文档保存接口后解锁:

  • 加锁操作时Client向Server发出lock请求,lockId放在请求头的X-WOPI-Lock字段中,Server获取该字段值后,判断文档是否被加锁或者校验请求lockId和已有是否匹配,匹配才能允许Client获取文档内容。
  • 解锁操作时Client向Server发出unlock请求,同样Server检查lockId是否匹配,匹配成功才能释放锁

看到这里有个疑问,为何lockId还需要Server来存储,Server没必要感知到锁的存在?上面提到Server通过SupportsLocks属性通知Client是否支持对文档加解锁,Server虽然不负责lockId的生成,但是具体加解锁判断成功与否的策略由Server控制,举个例子,如果两个用户A、B操作同一个文档,A先加锁拿到lockID,B后续通过unlock请求解锁表示要提前保存文档,并且解锁请求头携带的lockId和A相同,这种情况下Client无法判断是否允许这样操作,Client生成的lockId和具体用户没有任何关联,只有Server有权决定不同用户拿到相同lockId时操作是否允许。

思考

在文档编辑器领域微软是业界的标杆,它设计了好几种开放协议,包括本文讨论的WOPI协议,还有后来者Vscode为了支持多语言的语法补全功能提出了LSP协议等,这些平台设计思路是类似的:希望开发者能基于协议共建平台的生态,放大平台的价值和产品生命力。

这也是B端产品和技术值得学习和借鉴的思路;为了避免重复建设,针对数据分析、报表类等B端复杂平台,企业内部是不是也可以统一建设类似Office这样的平台,通过约定和开放数据源格式,以及类似WOPI这样操作接口协议,各个业务团队可以直接集成公共的报表平台以展示或者编辑报表。这考验企业内部各团队之间的博弈,虽然过程很难,但作为一种可选的集成方案,是公司内值得探讨的方向。