只介绍集成 不介绍搭建环境
一、整体流程
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 "";
}
}
}