Office365:WOPI集成

背景

前段时间,做了一个关于如何集成Office365的调研,探索如何将它集成到应用里面,方便多人的协同工作,这种应用场景特别在内部审计平台使用特别多,一些文档需要被不同角色查看,评论以及审批。

技术方案简介

通过快速的调研,发现已经有比较成熟的方案,其中之一就是微软定义的WOPI接口,只要严格按照其定义的规范,并实现其接口,就可以很快实现Office365的集成。

image.png

上面架构图,摘取至http://wopi.readthedocs.io/en/latest/overview.html,简单讲讲,整个技术方案,共有三个子系统:

  • 自建的前端业务系统
  • 自建的WOPI服务 - WOPI是微软的web application open platform interface-Web应用程序开放平台接口
  • Office online

我们可以通过iframe的方式把office online内嵌到业务系统,并且回调我们的WOPI服务进行相应的文档操作。

界面

界面的原型,通过iframe的方式,把office 365内嵌到了我们的业务页面,我们可以在这个页面上,多人协同对底稿进行查看和编辑。

image.png

样例代码如下:

class Office extends Component {
  render() {
    return (
      
(this.office_form = el)} name="office_form" target="office_frame" action={OFFICE_ONLINE_ACTION_URL} method="post" >
(this.frameholder = el)} />
); } 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(); } }

对前端应用来说,最需要知道的就是请求的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

视具体情况,请根据Wopi Discovery选择合适的API:

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

交互图

接下来就是具体的交互流程了, 我们先来到了业务系统,然后前端系统会在调用后端服务,获取相应的信息,比如access token还有即将访问的URL, 然后当用户查看或者编辑底稿的时候,前端系统会调用office365,它又会根据我们传的url参数,回调WOPI服务,进行一些列的操作,比如,它会调用API获取相应的文档基本信息,然后再发一次API请求获取文档的具体内容,最后就可以实现文档的在线查看和编辑,并且把结果通过WOPI的服务进行保存。

image.png

WOPI服务端接口如下:

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

    private WopiProtocalService wopiProtocalService;

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

    @GetMapping("/files/{name}/contents")
    public ResponseEntity 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 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里面包含了具体对接口的实现:

@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 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 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;
    }
}

具体实现细节,请参加如下代码库:

  • https://github.com/qinnnyul/office-365-wopi
  • https://github.com/qinnnyul/office-365-frontend

WOPI架构特点

image.png
  • 数据存放在内部存储系统(私有云或者内部数据中心),信息更加安全。
  • 自建WOPI服务,服务化,易于重用,且稳定可控。
  • 实现了WOPI协议,理论上可以集成所有Office在线应用,支持在线协作,扩展性好。
  • 解决方案成熟,微软官方推荐和提供支持。

WOPI开发依赖

  • 需要购买Office的开发者账号(个人的话,可以申请一年期的免费账号:https://developer.microsoft.com/en-us/office/profile/
    )。
  • WOPI服务测试、上线需要等待微软团队将URL加入白名单(测试环境大约需要1到3周的时间,才能完成白名单)。
  • 上线流程需要通过微软安全、性能等测试流程。

具体流程请参加:https://wopi.readthedocs.io/en/latest/build_test_ship/settings.html

参考

  • http://wopi.readthedocs.io/en/latest/overview.html

  • https://github.com/Microsoft/Office-Online-Test-Tools-and-Documentation

你可能感兴趣的:(Office365:WOPI集成)