Office365:WOPI集成

背景

前段時(shí)間尔崔,做了一個(gè)關(guān)于如何集成Office365的調(diào)研,探索如何將它集成到應(yīng)用里面仪搔,方便多人的協(xié)同工作忧换,這種應(yīng)用場(chǎng)景特別在內(nèi)部審計(jì)平臺(tái)使用特別多恬惯,一些文檔需要被不同角色查看,評(píng)論以及審批亚茬。

技術(shù)方案簡(jiǎn)介

通過(guò)快速的調(diào)研酪耳,發(fā)現(xiàn)已經(jīng)有比較成熟的方案,其中之一就是微軟定義的WOPI接口刹缝,只要嚴(yán)格按照其定義的規(guī)范碗暗,并實(shí)現(xiàn)其接口,就可以很快實(shí)現(xiàn)Office365的集成梢夯。

image.png

上面架構(gòu)圖言疗,摘取至http://wopi.readthedocs.io/en/latest/overview.html,簡(jiǎn)單講講颂砸,整個(gè)技術(shù)方案噪奄,共有三個(gè)子系統(tǒng):

  • 自建的前端業(yè)務(wù)系統(tǒng)
  • 自建的WOPI服務(wù) - WOPI是微軟的web application open platform interface-Web應(yīng)用程序開放平臺(tái)接口
  • Office online

我們可以通過(guò)iframe的方式把office online內(nèi)嵌到業(yè)務(wù)系統(tǒng),并且回調(diào)我們的WOPI服務(wù)進(jìn)行相應(yīng)的文檔操作人乓。

界面

界面的原型勤篮,通過(guò)iframe的方式,把office 365內(nèi)嵌到了我們的業(yè)務(wù)頁(yè)面色罚,我們可以在這個(gè)頁(yè)面上叙谨,多人協(xié)同對(duì)底稿進(jìn)行查看和編輯。

image.png

樣例代碼如下:

class Office extends Component {
  render() {
    return (
      <div className="office">
        <form
          id="office_form"
          ref={el => (this.office_form = el)}
          name="office_form"
          target="office_frame"
          action={OFFICE_ONLINE_ACTION_URL}
          method="post"
        >
          <input name="access_token" value={ACCESS_TOKEN_VALUE} type="hidden" />
          <input
            name="access_token_ttl"
            value={ACCESS_TOKEN_TTL_VALUE}
            type="hidden"
          />
        </form>
        <span id="frameholder" ref={el => (this.frameholder = el)} />
      </div>
    );
  }

  componentDidMount() {
    const office_frame = document.createElement('iframe');
    office_frame.name = 'office_frame';
    office_frame.id = 'office_frame';
    office_frame.title = 'Office Online';
    office_frame.setAttribute('allowfullscreen', 'true');
    this.frameholder.appendChild(office_frame);
    this.office_form.submit();
  }
}

對(duì)前端應(yīng)用來(lái)說(shuō)保屯,最需要知道的就是請(qǐng)求的API URL手负,e.g:

https://word-view.officeapps-df.live.com/wv/wordviewerframe.aspx?WOPISrc={your_wopi_service_dns}/wopi/files/
https://word-edit.officeapps-df.live.com/we/wordeditorframe.aspx?WOPISrc={your_wopi_service_dns}/wopi/files/demo.docx

視具體情況,請(qǐng)根據(jù)Wopi Discovery選擇合適的API:

https://wopi.readthedocs.io/en/latest/discovery.html

交互圖

接下來(lái)就是具體的交互流程了姑尺, 我們先來(lái)到了業(yè)務(wù)系統(tǒng)竟终,然后前端系統(tǒng)會(huì)在調(diào)用后端服務(wù),獲取相應(yīng)的信息切蟋,比如access token還有即將訪問(wèn)的URL统捶, 然后當(dāng)用戶查看或者編輯底稿的時(shí)候,前端系統(tǒng)會(huì)調(diào)用office365柄粹,它又會(huì)根據(jù)我們傳的url參數(shù)喘鸟,回調(diào)WOPI服務(wù),進(jìn)行一些列的操作驻右,比如什黑,它會(huì)調(diào)用API獲取相應(yīng)的文檔基本信息,然后再發(fā)一次API請(qǐng)求獲取文檔的具體內(nèi)容堪夭,最后就可以實(shí)現(xiàn)文檔的在線查看和編輯愕把,并且把結(jié)果通過(guò)WOPI的服務(wù)進(jìn)行保存拣凹。

image.png

WOPI服務(wù)端接口如下:

@RestController
@RequestMapping(value = "/wopi")
public class WopiProtocalController {

    private WopiProtocalService wopiProtocalService;

    @Autowired
    public WopiProtocalController(WopiProtocalService wopiProtocalService) {
        this.wopiProtocalService = wopiProtocalService;
    }

    @GetMapping("/files/{name}/contents")
    public ResponseEntity<Resource> getFile(@PathVariable(name = "name") String name, HttpServletRequest request) throws UnsupportedEncodingException, FileNotFoundException {
        return wopiProtocalService.handleGetFileRequest(name, request);
    }

    @PostMapping("/files/{name}/contents")
    public void putFile(@PathVariable(name = "name") String name, @RequestBody byte[] content, HttpServletRequest request) throws IOException {
        wopiProtocalService.handlePutFileRequest(name, content, request);
    }


    @GetMapping("/files/{name}")
    public ResponseEntity<CheckFileInfoResponse> getFileInfo(@PathVariable(name = "name") String name, HttpServletRequest request) throws UnsupportedEncodingException, FileNotFoundException {
        return wopiProtocalService.handleCheckFileInfoRequest(name, request);
    }

    @PostMapping("/files/{name}")
    public ResponseEntity editFile(@PathVariable(name = "name") String name, HttpServletRequest request) {
        return wopiProtocalService.handleEditFileRequest(name, request);
    }

}

WopiProtocalService里面包含了具體對(duì)接口的實(shí)現(xiàn):

@Service
public class WopiProtocalService {

    @Value("${localstorage.path}")
    private String filePath;

    private WopiAuthenticationValidator validator;
    private WopiLockService lockService;

    @Autowired
    public WopiProtocalService(WopiAuthenticationValidator validator, WopiLockService lockService) {
        this.validator = validator;
        this.lockService = lockService;
    }

    public ResponseEntity<Resource> handleGetFileRequest(String name, HttpServletRequest request) throws UnsupportedEncodingException, FileNotFoundException {
        this.validator.validate(request);
        String path = filePath + name;
        File file = new File(path);
        InputStreamResource resource = new InputStreamResource(new FileInputStream(file));

        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Disposition", "attachment;filename=" +
                new String(file.getName().getBytes("utf-8"), "ISO-8859-1"));

        return ResponseEntity.ok()
                .headers(headers)
                .contentLength(file.length())
                .contentType(MediaType.parseMediaType("application/octet-stream"))
                .body(resource);
    }

    /**
     * @param name
     * @param content
     * @param request
     * @TODO: rework on it based on the description of document
     */
    public void handlePutFileRequest(String name, byte[] content, HttpServletRequest request) throws IOException {
        this.validator.validate(request);
        Path path = Paths.get(filePath + name);
        Files.write(path, content);
    }

    public ResponseEntity<CheckFileInfoResponse> handleCheckFileInfoRequest(String name, HttpServletRequest request) throws UnsupportedEncodingException, FileNotFoundException {
        this.validator.validate(request);
        CheckFileInfoResponse info = new CheckFileInfoResponse();
        String fileName = URLDecoder.decode(name, "UTF-8");
        if (fileName != null && fileName.length() > 0) {
            File file = new File(filePath + fileName);
            if (file.exists()) {
                info.setBaseFileName(file.getName());
                info.setSize(file.length());
                info.setOwnerId("admin");
                info.setVersion(file.lastModified());
                info.setAllowExternalMarketplace(true);
                info.setUserCanWrite(true);
                info.setSupportsUpdate(true);
                info.setSupportsLocks(true);
            } else {
                throw new FileNotFoundException("Resource not found/user unauthorized");
            }
        }
        return ResponseEntity.ok().contentType(MediaType.parseMediaType(MediaType.APPLICATION_JSON_UTF8_VALUE)).body(info);
    }

    public ResponseEntity handleEditFileRequest(String name, HttpServletRequest request) {
        this.validator.validate(request);
        ResponseEntity responseEntity;
        String requestType = request.getHeader(WopiRequestHeader.REQUEST_TYPE.getName());
        switch (valueOf(requestType)) {
            case PUT_RELATIVE_FILE:
                responseEntity = this.handlePutRelativeFileRequest(name, request);
                break;
            case LOCK:
                if (request.getHeader(WopiRequestHeader.OLD_LOCK.getName()) != null) {
                    responseEntity = this.lockService.handleUnlockAndRelockRequest(name, request);
                } else {
                    responseEntity = this.lockService.handleLockRequest(name, request);
                }
                break;
            case UNLOCK:
                responseEntity = this.lockService.handleUnLockRequest(name, request);
                break;
            case REFRESH_LOCK:
                responseEntity = this.lockService.handleRefreshLockRequest(name, request);
                break;
            case UNLOCK_AND_RELOCK:
                responseEntity = this.lockService.handleUnlockAndRelockRequest(name, request);
                break;
            default:
                throw new UnSupportedRequestException("Operation not supported");
        }
        return responseEntity;
    }
}

具體實(shí)現(xiàn)細(xì)節(jié),請(qǐng)參加如下代碼庫(kù):

WOPI架構(gòu)特點(diǎn)

image.png
  • 數(shù)據(jù)存放在內(nèi)部存儲(chǔ)系統(tǒng)(私有云或者內(nèi)部數(shù)據(jù)中心)恨豁,信息更加安全嚣镜。
  • 自建WOPI服務(wù),服務(wù)化橘蜜,易于重用菊匿,且穩(wěn)定可控。
  • 實(shí)現(xiàn)了WOPI協(xié)議计福,理論上可以集成所有Office在線應(yīng)用捧请,支持在線協(xié)作,擴(kuò)展性好棒搜。
  • 解決方案成熟疹蛉,微軟官方推薦和提供支持。

WOPI開發(fā)依賴

  • 需要購(gòu)買Office的開發(fā)者賬號(hào)(個(gè)人的話力麸,可以申請(qǐng)一年期的免費(fèi)賬號(hào):https://developer.microsoft.com/en-us/office/profile/
    )可款。
  • WOPI服務(wù)測(cè)試、上線需要等待微軟團(tuán)隊(duì)將URL加入白名單(測(cè)試環(huán)境大約需要1到3周的時(shí)間克蚂,才能完成白名單)闺鲸。
  • 上線流程需要通過(guò)微軟安全、性能等測(cè)試流程埃叭。

具體流程請(qǐng)參加:https://wopi.readthedocs.io/en/latest/build_test_ship/settings.html

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末摸恍,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子赤屋,更是在濱河造成了極大的恐慌立镶,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件类早,死亡現(xiàn)場(chǎng)離奇詭異媚媒,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)涩僻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門缭召,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人逆日,你說(shuō)我怎么就攤上這事嵌巷。” “怎么了室抽?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵搪哪,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我狠半,道長(zhǎng)噩死,這世上最難降的妖魔是什么颤难? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任神年,我火速辦了婚禮已维,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘已日。我一直安慰自己垛耳,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布飘千。 她就那樣靜靜地躺著堂鲜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪护奈。 梳的紋絲不亂的頭發(fā)上缔莲,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音霉旗,去河邊找鬼痴奏。 笑死,一個(gè)胖子當(dāng)著我的面吹牛厌秒,可吹牛的內(nèi)容都是我干的读拆。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼鸵闪,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼檐晕!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起蚌讼,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤辟灰,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后篡石,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體伞矩,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年夏志,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了乃坤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡沟蔑,死狀恐怖湿诊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情瘦材,我是刑警寧澤厅须,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站食棕,受9級(jí)特大地震影響朗和,放射性物質(zhì)發(fā)生泄漏错沽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一眶拉、第九天 我趴在偏房一處隱蔽的房頂上張望千埃。 院中可真熱鬧,春花似錦忆植、人聲如沸放可。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)耀里。三九已至,卻和暖如春拾氓,著一層夾襖步出監(jiān)牢的瞬間冯挎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工咙鞍, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留房官,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓奶陈,卻偏偏與公主長(zhǎng)得像易阳,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子吃粒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容