Spring-boot+Vue 前后端快速集成onlyoffice_springboot onlyoffice-程序员宅基地

技术标签: java  工具  环境搭建  

只介绍集成 不介绍搭建环境

一、整体流程

onlyoffice服务使用docker部署
在这里插入图片描述
onlyoffice-documentserver: 文档服务 文件转换服务 编辑器服务
rabbitmq: 消息队列
postgres:9.5: 数据库


docker-compose.yml 文件

version: '2'
services:
  onlyoffice-documentserver:
    container_name: onlyoffice-documentserver
    image: sgcc-dky-smartky-onlyoffice_onlyoffice-documentserver
    depends_on:
      - onlyoffice-postgresql
      - onlyoffice-rabbitmq
    environment:
      - DB_TYPE=postgres
      - DB_HOST=onlyoffice-postgresql
      - DB_PORT=5432
      - DB_NAME=onlyoffice
      - DB_USER=onlyoffice
      - AMQP_URI=amqp://guest:guest@onlyoffice-rabbitmq
      # Uncomment strings below to enable the JSON Web Token validation.
      #- JWT_ENABLED=true
      #- JWT_SECRET=secret
      #- JWT_HEADER=Authorization
      #- JWT_IN_BODY=true
    ports:
      - '8088:80'
      - '443:443'
    stdin_open: true
    restart: always
    stop_grace_period: 60s
    volumes:
      - /var/www/onlyoffice/Data
      - /var/log/onlyoffice
      - /var/lib/onlyoffice/documentserver/App_Data/cache/files
      - /var/www/onlyoffice/documentserver-example/public/files
      - /usr/share/fonts

  onlyoffice-rabbitmq:
    container_name: onlyoffice-rabbitmq
    image: rabbitmq
    restart: always
    expose:
      - '5672'
    ports:
      - '5672:5672'

  onlyoffice-postgresql:
    container_name: onlyoffice-postgresql
    image: postgres:9.5
    environment:
      - POSTGRES_DB=onlyoffice
      - POSTGRES_USER=onlyoffice
      - POSTGRES_HOST_AUTH_METHOD=trust
    restart: always
    expose:
      - '5432'
    ports:
      - '5432:5432'
    volumes:
      - postgresql_data:/var/lib/postgresql

volumes:
  postgresql_data:


访问文档时序图

在这里插入图片描述

文档保存时序图

在这里插入图片描述



二、前端集成

前端集成过程

OnlyOffice前端由onlyOffice-documentServer提供,后端会生成api.js文件,前端引入这个js就可以集成onlyOffice的编辑器。


前端项目结构

在这里插入图片描述
env.js 是配置文件 配置onlyoffice编辑器以及文档服务的地址

var merge = require('webpack-merge')
var prodEnv = require('./prod.env')

module.exports = merge(prodEnv, {
    
    NODE_ENV: '"development"',
    BASE_URL: '"/api"',
    // onlyoffice document-server 服务端口
    // 编辑地址
    ONLYOFFICE_DOCUMENT_HOST: '"http://127.0.0.1"',
    // 预览地址
    ONLYOFFICE_DOCUMENT_PREVIEW_HOST: '"http://127.0.0.1"',
    ONLYOFFICE_DOCUMENT_PORT: 8080,
    // onlyoffice前端编辑器地址
    ONLYOFFICE_DOCUMENT_URL: '"/web-apps/apps/api/documents/api.js"'
});

onlyofficeView是onlyoffice 基础组件 封装了核心的onlyoffice编辑器

<template>
  <div id="monitorOffice"></div>
</template>
<script>
import {
      handleDocType} from "./docType";

export default {
      
  name: "onlyofficeView",
  ...省略,
  methods: {
      
    initEditor() {
      
      // 加入 onlyoffice api.js 脚本
      const apiScriptDom = document.getElementById("onlyoffice-document-api");
     
      const script = document.createElement("script");
      const {
      protocol} = window.location;

      const ONLYOFFICE_DOCUMENT_PORT=process.env.ONLYOFFICE_DOCUMENT_PORT;
      
      const ONLYOFFICE_DOCUMENT_URL=process.env.ONLYOFFICE_DOCUMENT_URL;
      
      script.setAttribute("id", "onlyoffice-document-api");
      script.setAttribute(
        "src",
        `${
        this.documentUrl +
        ":" +
        ONLYOFFICE_DOCUMENT_PORT +
        ONLYOFFICE_DOCUMENT_URL}`
      );
      document.body.appendChild(script);
      const _self = this;
      script.onload = () => {
      
        _self.scriptReady = true;
        console.log("initEditor 初始化组件完成!");
        if (_self.option.url) {
      
          _self.setEditor(_self.option);
        }
      };
    },
    setEditor(option) {
      
      const {
      
        $store: {
      
          state: {
      
            user: {
      userId, nickname}
          }
        }
      } = this;
      this.doctype = handleDocType(option.fileType);
    }
  },
  watch: {
      
    option: {
      
      handler: function (n, o) {
      
        this.doctype = handleDocType(n.fileType);
        if (this.scriptReady) {
      
          this.setEditor(n);
        }
      },
      deep: true
    }
  }
};
</script>

onlineCat onlineEdit 是集成了onlyoffice编辑器的编辑和预览 主要是根据各种业务场景初始化的配置

<template>
  <div style="height:100%">
    <onlyoffice-view ref="onlyOffcieView" :option="option" :document-url="documentUrl"></onlyoffice-view>
  </div>
</template>



三、后端集成

在这里插入图片描述

1.核心服务接口

核心方法
下载 download()
保存 save()
获取onlyoffice Token getToken()
校验token verifyToken()

/**
 * onlyoffice 服务接口
 *
 * @author: colagy
 * 2021-03-26 16:06
 */
public interface IOnlyofficeService {
    

    /**
     * onlyoffice document server 下载文件
     * @param id
     * @param scene
     * @param userId
     * @param timeStamp
     * @param token
     * @param response
     * @throws IOException
     */
    void download(String id, Enum scene, String userId, Long timeStamp, String token, HttpServletResponse response) throws IOException;

    /**
     * 文件回写
     *
     * @param request onlyoffce document server 请求
     * @param eClass  文档场景枚举 类对象
     * @param <E>     onlyofficeScene 文档场景枚举
     * @return
     * @throws IOException
     */
    <E extends Enum> JSONObject save(HttpServletRequest request, Class<E> eClass) throws IOException;

    /**
     * @return
     */
    OnlyofficeToken getToken();

    boolean verifyToken(String userId, Long timeStamp, String token, Long expire);
}

2.服务基类

定义了两个 抽象方法 子类需要实现并返回 存储方法 和 token过期校验

@Service
public abstract class SimpleOnlyofficeService implements IOnlyofficeService {
    
    private final static Logger log = LoggerFactory.getLogger(SimpleOnlyofficeService.class);

    private static final String tokenPrefix = "onlyoffice_token_profix_node_server";

    public abstract IOnlyofficeStorage getOnlyofficeStorage();

    public abstract long getTokenExpire();

	...
	         
}

默认实现的download()方法
调用getOnlyofficeStorage 获取存储的实现类 可以是oss 或者本地文件存储
调用onlyofficeStorage.get()方法 返回值是inputStream写入响应流返回给onlyofficeDocumentServer

	@Override
    public void download(String id, Enum scene, String userId, Long timeStamp, String token, HttpServletResponse response) throws IOException {
    
        // token过期时间默认10分钟
        long tokenExpire = getTokenExpire();
        if (tokenExpire <= 0) {
    
            tokenExpire = 10 * 60 * 1000L;
        }
        if (!verifyToken(userId, timeStamp, token, tokenExpire)) {
    
            throw new CustomGenericException("授权已过期或授权失败");
        }

        IOnlyofficeStorage onlyofficeStorage = getOnlyofficeStorage();
        InputStream inputStream = onlyofficeStorage.get(id, scene);

        FileUtil.nioTransferTo(inputStream, response.getOutputStream());
        // TODO 需要storage 返回更多信息
        FileUtil.parseDownloadResponse(response, String.valueOf(timeStamp), "application/octet-stream");
    }

默认实现的save()方法 需要返回 {“error”:0} 给onlyoffice确认
调用 onlyofficeStorage.put(id, scene, inputStream, bytes.length)方法 将onlyOfficeServer传入的流保存

	@Override
    public <E extends Enum> JSONObject save(HttpServletRequest request, Class<E> eClass) throws IOException {
    
        Scanner scanner = new Scanner(request.getInputStream()).useDelimiter("\\A");
        String bodyStr = scanner.hasNext() ? scanner.next() : "";

        Map<String, String[]> parameterMap = request.getParameterMap();
        String[] ids = parameterMap.get("id");
        String[] scenes = parameterMap.get("scene");

        String id = null;
        if (ArrayUtils.isNotEmpty(ids)) {
    
            id = ids[0];
        }
        if (Objects.isNull(id)) {
    
            JSONObject res = new JSONObject();
            res.put("error", -1);
            return res;
        }

        Enum scene = null;
        if (ArrayUtils.isNotEmpty(scenes) && EnumUtils.isValidEnum(eClass, scenes[0])) {
    
            try {
    
                scene = Enum.valueOf(eClass, scenes[0]);
            } catch (Exception e) {
    
                String msg = "onlyoffice save方法 未指定scene";
                log.error(ErrorUtil.getErrorStack(e, msg));
            }
        }
        if (Objects.isNull(scene)) {
    
            JSONObject res = new JSONObject();
            res.put("error", -1);
            return res;
        }

        JSONObject body = JSONObject.parseObject(bodyStr);

        int status = body.getInteger("status");

        if (status == 2) {
    

            String downloadUrl = body.getString("url");
            if (StringUtils.isNotBlank(downloadUrl)) {
    
                IOnlyofficeStorage onlyofficeStorage = getOnlyofficeStorage();

                byte[] bytes = HttpUtil.downloadBytes(downloadUrl);
                InputStream inputStream = new ByteArrayInputStream(bytes);

				// 默认方法 可以实现beforeSave
                beforeSave(id, scene, parameterMap, body, inputStream);
                onlyofficeStorage.put(id, scene, inputStream, bytes.length);
                afterSave(id, scene, parameterMap, body, inputStream);

            }
        }

        // 正常处理需要返回 {"error":0} 给onlyoffice确认
        JSONObject res = new JSONObject();
        res.put("error", 0);
        return res;
    }



四、权限验证

服务基类定义getUserId()方法 实现类需要返回一个用户唯一标识

	/**
     * 必须实现返回当前登录用户id的方法 用于token获取以及校验
     *
     * @return
     */
    protected abstract String getUserId();

    /**
     * 获取token
     *
     * @return
     */
    @Override
    public OnlyofficeToken getToken() {
    
        String userId = getUserId();

        long timeStamp = System.currentTimeMillis();
        String tokenStr = parseToken(userId, timeStamp);

        OnlyofficeToken token = new OnlyofficeToken(userId, timeStamp, tokenStr);

        return token;
    }

    private String parseToken(String userId, Long timeStamp) {
    
        String tokenStr = "使用一些加密的方法将userId timeStamp 加密返回一个加密后的token,可以参加jwt";
        return tokenStr;
    }

    /**
     * 校验token 并判断过期时间
     *
     * @param token  token
     * @param expire 过期时间(ms) 默认 10*60*000ms 10分钟
     * @return
     */
    @Override
    public boolean verifyToken(String userId, Long timeStamp, String token, Long expire) {
    
        if (StringUtils.isBlank(userId)) {
    
            log.warn("onlyofficeToken 验证失败 userId为空");
            return false;
        }
        if (Objects.isNull(timeStamp) || timeStamp == 0) {
    
            log.warn("onlyofficeToken 验证失败 timeStamp为空");
            return false;
        }
        if (Objects.isNull(expire)) {
    
            expire = 10 * 60 * 1000L;
        }


        long now = System.currentTimeMillis();
        if (now - timeStamp > expire) {
    
            log.error("onlyoffice token已过期 , userId:" + userId + ", token:" + token + ", timeStamp:" + timeStamp + ", now:" + now + ", expire:" + expire);
            return false;
        }

        String realToken = parseToken(userId, timeStamp);
        boolean isValid = StringUtils.equals(token, realToken);
        if (!isValid) {
    
            log.error("onlyoffice token验证失败! " + " token:" + token + ", realToken:" + realToken + ", userId:" + userId + ", timeStamp:" + timeStamp + ", expire:" + expire);
        }
        return StringUtils.equals(token, realToken);
    }




五、储存集成

1.存储接口

主要是两个方法
get() 返回字节流
put() 传入字节流
可以用oss实现 也可以用本地文件实现 只要能返回字节流和写入字节流就可以

/**
 * onlyoffice 在线编制的 储存方式需要实现这个接口
 *
 * @author: colagy
 * 2021-03-29 11:03
 */
public interface IOnlyofficeStorage {
    
    /**
     * 获取文件字节流
     *
     * @param id    文件唯一标识
     * @param scene 使用场景
     * @return
     * @throws IOException
     */
    InputStream get(String id, Enum scene) throws IOException;

    /**
     * 写入文件字节流
     *
     * @param id          文件唯一标识
     * @param scene       使用场景
     * @param inputStream 输入流
     * @param inSize      流大小(用于nio拷贝)
     * @throws IOException
     */
    void put(String id, Enum scene, InputStream inputStream, long inSize) throws IOException;
}
2.本地文件基类

子类需要实现 getFilePath() 根据资源唯一标识返回文件的储存路径

/**
 * 文件系统存储抽象类 子类需要实现通过id获取存储路径的方法
 *
 * @author: colagy
 * 2021-03-29 11:07
 */
public abstract class FsStorage implements IOnlyofficeStorage {
    
    public abstract String getFilePath(String id, Enum scene);

    @Override
    public InputStream get(String id, Enum scene) throws IOException {
    
        String saveFilePath = getFilePath(id, scene);
        Assert.notBlank(saveFilePath);

        File file = new File(saveFilePath);
        File parentFile = file.getParentFile();
        if (Objects.nonNull(parentFile)) {
    
            if (!parentFile.exists()) {
    
                parentFile.mkdirs();
            }
        }
        if (!file.exists()) {
    
            file.createNewFile();
        }

        Assert.isTrue(file.exists());

        FileInputStream fileInputStream = new FileInputStream(file);

        return fileInputStream;
    }

    @Override
    public void put(String id, Enum scene, InputStream inputStream, long inSize) throws IOException {
    
        String filePath = getFilePath(id, scene);
        FileUtil.nioTransferTo(inputStream, inSize, filePath);
    }

}



六、场景集成

目前文件存储需要集成文件场景
不同的场景可以有不同的方式获取文件路径

/**
 * 场景需要实现这个接口
 *
 * @author: colagy
 * 2021-06-11 17:10
 */
public interface IFsScene {
    
    String getFilePath(String id);
}

文件场景默认实现类 base64方式 只做示例不推荐使用

/**
 * 文件路径base64
 *
 * @author: colagy
 * 2021-06-15 12:03
 */
@Service(value = "FS_BASE64")
public class Base64FsScene implements IFsScene {
    
    /**
     * id为filePath的base64值 把base64解析就是filePath
     * TODO 前端base64需要使用 window.btoa(window.encodeURIComponent("/filepath/文档.doc")) 转base64
     *
     * @param filePathBase64 filePath的base64值
     * @return file path
     */
    @Override
    public String getFilePath(String filePathBase64) {
    
        if (StringUtils.isBlank(filePathBase64)) {
    
            return "";
        }

        try {
    
            Base64.Decoder decoder = Base64.getDecoder();
            byte[] decode = decoder.decode(filePathBase64);
            String urlEncodeFilePath = new String(decode);
            String filePath = URLDecoder.decode(urlEncodeFilePath, "utf-8");
            return filePath;
        } catch (Exception e) {
    
            return "";
        }
    }
}




版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/QQ909988322/article/details/118283001

智能推荐

史上最全的渗透测试面试题_360渗透面试题-程序员宅基地

文章浏览阅读827次。一、信息收集1.获取域名的whois信息,获取注册者邮箱姓名电话等。2.通过站长之家、明小子、k8等查询服务器旁站以及子域名站点,因为主站一般比较难,所以先看看旁站有没有通用性的cms或者其他漏洞。3、通过DNS域传送漏洞、备份号查询、SSl证书、APP、微信公众号、暴力破解、DNS历史记录、K8 C段查询、Jsfinder、360或华为威胁情报、证书序列号获取企业域名与ip。4.通过Nmap、Wappalyzer、御剑等查看服务器操作系统版本,web中间件,看看是否存在已知的漏洞,比如IIS_360渗透面试题

Django分页_django 分页效率慢-程序员宅基地

文章浏览阅读1.1k次。Paginator对象方法init(列表,int):返回分页对象,参数为列表数据,每面数据的条数属性count:返回对象总数属性num_pages:返回页面总数属性page_range:返回页码列表,从1开始,例如[1, 2, 3, 4]方法page(m):返回Page对象,表示第m页的数据,下标以1开始Page对象_django 分页效率慢

Python爬虫超详细讲解(零基础入门,幼儿园小朋友都看的懂)_儿童爬虫学的是什么-程序员宅基地

文章浏览阅读527次。前言:领取python相关资料可以进q裙777899409免费领取、每晚还有大厂老师直播教学、学习路线、电子书籍,python学习相关资料领取讲解我们的爬虫之前,先概述关于爬虫的简单概念(毕竟是零基础教程)爬虫网络爬虫(又被称为网页蜘蛛,网络机器人)就是模拟浏览器发送网络请求,接收请求响应,一种按照一定的规则,自动地抓取互联网信息的程序。原则上,只要是浏览器(客户端)能做的事情,爬虫都能够做。为什么我们要使用爬虫互联网大数据时代,给予我们的是生活的便利以及海量数据爆炸式.._儿童爬虫学的是什么

C程序argc、argv的使用_c语言argc和argv怎么使用-程序员宅基地

文章浏览阅读7.4k次,点赞9次,收藏28次。前提:C文件编译、汇编后生成的.exe文件,就可以通过命令行来执行该exe文件。 命令行执行.exe文件,用微软推出的powershell,格式是: start ***.exe 或者 start " ***.exe ",用powershell无法输入argv,总是执行不了。所以推荐使用windows自带的cmd。用cmd执行.exe步骤:1. 先找到.exe文件路径:不同的adk(我也不知道adk、编译器有什么区别,大佬别喷)生成的.exe文件位置不同,找一下就好。复制文件..._c语言argc和argv怎么使用

c语言获取当前日期和时间_c语言获取当前年月日-程序员宅基地

文章浏览阅读1.5w次,点赞17次,收藏122次。c语言获取当前日期和时间`time_t`类型:日历时间`time`函数:获取当前日历时间tm 结构体:分解时间`localtime`函数:从日历时间转换为分解时间代码_c语言获取当前年月日

erlang语言详解并推荐入门书籍(10本)-程序员宅基地

文章浏览阅读1.9k次。Erlang是一种函数式编程语言,它旨在用于高可靠性、分布式、并发的系统。Erlang最初是由爱立信公司的Joe Armstrong等人开发的,用于管理电话交换机系统,具有高可靠性和容错能力。Erlang的语法基于模式匹配和递归,使用虚拟机运行,可以提供轻量级进程和分布式消息传递机制,支持OTP(开放式电信平台)。虚拟机Erlang运行于虚拟机BEAM(Bogdan/Björn's Erlang Abstract Machine),它是一个高度优化的虚拟机,可以提供实时垃圾回收和进程调度机制。_erlang语言

随便推点

游戏常用数据分析指标汇总_游戏指标-程序员宅基地

文章浏览阅读1.5w次,点赞31次,收藏141次。乐元素移动游戏运营数据分析指标汇总一、用户获取1、mobile用户获取流程点击-下载-安装-激活-注册-DNU点击:点击广告页或者点击广告链接数 下载:点击后成功下载用户数 安装:下载程序并成功安装用户数 激活:成功安装并首次激活应用程序 注册:产生user_id DNU:产生user_id并且首次登陆关注问题:关注Mobile游戏从推广到DNU每个步骤的转换,提高..._游戏指标

wincc与数据库sql server之间的数据存储_wincc如何往sql里写数据-程序员宅基地

文章浏览阅读3.1k次,点赞4次,收藏15次。实现目标:1、把wincc中变量,存储到SQL数据库中2、查询数据库内容,在控件中显示步骤:1、软件wincc 7.3、wincc安装自带的sql server 2008 R22、新建数据库3、新建wincc项目--变量建立4、所用到的控件5、画面打开脚本:主要实现最新数据显示、连接数据库、控件设置等SubOnOpen()DimDimQRDim'对应表格控件名称DimDimDimzxy1'查询当天全部数据,除了控件名称要注意修改外,以上其他为标准Set。_wincc如何往sql里写数据

Angular应用中tsconfig.json文件配置说明及配置全局路径映射_angular的config.json里面的内容是you什么决定的-程序员宅基地

文章浏览阅读1.5k次。Angular应用中tsconfig.json文件配置说明及如何配置全局路径映射_angular的config.json里面的内容是you什么决定的

C语言—打单词游戏_c语言命令行单词游戏-程序员宅基地

文章浏览阅读1.5k次,点赞2次,收藏11次。ps:页面最下面附上程序打包,也可在线浏览打单词游戏:本项目,主要目的是练习模块划分能力,我将整个程序划分为多个模块项目分析:显示要求:打单词下落方式显示多个单词通过正确输入消除单词对输入正确性统计能够随着熟练度改变速度有软件版权信息好看点,更有吸引力隐式要求:单词来源(外部文件或是字符串)原始单词存储问题(多个同类型元素,采用数组的方式,每个数组元素为字符串单词的地..._c语言命令行单词游戏

基于Django的深度学习视频分类Web系统_深度学习+django-程序员宅基地

文章浏览阅读834次。Temporal shift module数据处理:视频是按特定顺序排列的一组图像的集合,这些图像也称为帧。视频分类任务需要先对短视频进行解码,然后再将输出的图像帧序列灌入到VideoTag中进行训练和预测。图像建模:先从训练数据中,对每个类别均匀采样少量样本数据,构成十万量级的训练视频。然后使用TSN网络进行训练,提取所有视频帧的TSN模型分类层前一层的特征数据。在这个过程中,每一帧都被转化成相应的特征向量,一段视频被转化成一个特征序列。序列学习:采用Attention clusters、LSTM和Nex_深度学习+django

C#——【关键字:MVVM】WPF数据绑定-MVVM架构-Caliburn.Micro_c# mvvm-程序员宅基地

文章浏览阅读1.3k次。一、MVVM架构是什么View、ViewModel、Model。ViewModel连接Model和View层,相当于把之前View层的Cs代码独立出来了二、优势劣势&适用场景MVVM最大的优势是编写前端逻辑非常复杂的页面,尤其是需要大量DOM操作的逻辑,利用MVVM可以极大地简化前端页面的逻辑。但是MVVM不是万能的,它的目的是为了解决复杂的前端逻辑。但对于以展示为主的页面,例如,新闻,博客、文档等,不能使用MVVM展示数据,因为这些页面需要被搜索引擎索引,而搜索引擎无法获取使用MV_c# mvvm