Docker 部署onlyoffice

以下是全面优化后的 OnlyOffice 部署、Token 生成、前端测试及文件转换方案,解决潜在问题、提升稳定性和易用性:

一、Docker 部署优化(稳定性+可维护性)

bash 复制代码
sudo docker run -itd \
  -p 1230:80 \
  --name onlyoffice \
  --restart=always \  # 容器异常自动重启
  --privileged=true \  # 解决文件权限问题
  -e JWT_SECRET=VI71S3cGtX75g96HgFW52785zQhblz1KwMc1Jzk \
  -e JWT_ENABLED=true \
  -e TZ=Asia/Shanghai \  # 同步时区,避免日志时间错乱
  -v /data/middleware/onlyoffice/DocumentServer/logs:/var/log/onlyoffice \
  -v /data/middleware/onlyoffice/DocumentServer/data:/var/www/onlyoffice/Data \
  -v /data/middleware/onlyoffice/DocumentServer/lib:/var/lib/onlyoffice \
  -v /data/middleware/onlyoffice/DocumentServer/rabbitmq:/var/lib/rabbitmq \
  -v /data/middleware/onlyoffice/DocumentServer/redis:/var/lib/redis \
  -v /data/middleware/onlyoffice/DocumentServer/db:/var/lib/postgresql \
  onlyoffice/documentserver:7.3

优化点

  1. 新增 --restart=always 确保服务高可用
  2. 增加 --privileged=true 解决宿主机与容器文件权限不匹配问题
  3. 配置 TZ=Asia/Shanghai 同步时区,日志时间更易排查
  4. 格式化命令换行,提升可读性

二、Token 生成工具类优化(健壮性+易用性)

java 复制代码
package org.example;

import java.util.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Objects;

/**
 * OnlyOffice JWT Token 生成工具(优化版)
 * 支持预览/编辑/转换三种模式,新增参数校验、异常处理、可配置化
 */
public class OnlyOfficeTokenGenerator {
    // 可配置常量(集中管理,便于修改)
    private static final String JWT_SECRET = "VI71S3cGtX75g96HgFW52785zQhblz1KwMc1Jzk";
    private static final ObjectMapper MAPPER = new ObjectMapper();
    private static final String ALGORITHM = "HS256";
    private static final String TYPE = "JWT";
    private static final int EXPIRATION_SECONDS = 3600; // 1小时有效期
    private static final String DEFAULT_LANG = "zh-CN"; // 默认中文

    // 单例模式(避免重复创建对象)
    private static final OnlyOfficeTokenGenerator INSTANCE = new OnlyOfficeTokenGenerator();
    private OnlyOfficeTokenGenerator() {}
    public static OnlyOfficeTokenGenerator getInstance() { return INSTANCE; }

    public static void main(String[] args) {
        try {
            // 配置参数(替换为实际值)
            String docUrl = "http://101.42.40.19:9000/xingyue-knowledge/工作需要1.0.docx";
            String docTitle = "工作需要1.0.docx";
            String fileType = "docx";
            String callbackUrl = "https://xingyue.asia/test/callback";

            // 生成 Token
            String viewToken = INSTANCE.generateViewToken(docUrl, docTitle, fileType, "word");
            System.out.println("预览 Token:\n" + viewToken + "\n");

            String editToken = INSTANCE.generateEditToken(docUrl, docTitle, fileType, "word", callbackUrl, "测试用户", "user123");
            System.out.println("编辑 Token:\n" + editToken + "\n");

            String convertToken = INSTANCE.generateConvertToken(docUrl, fileType, "pdf", "convert_"+System.currentTimeMillis(), docTitle);
            System.out.println("转换 Token:\n" + convertToken);
        } catch (Exception e) {
            System.err.println("Token 生成失败:" + e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 预览模式 Token(仅查看,无编辑权限)
     */
    public String generateViewToken(String docUrl, String docTitle, String fileType, String documentType) throws Exception {
        validateRequiredParams(
                new StringParam(docUrl, "文档URL"),
                new StringParam(docTitle, "文档标题"),
                new StringParam(fileType, "文件类型"),
                new StringParam(documentType, "文档类别")
        );
        return generateJWT(createDocumentPayload("view", docUrl, docTitle, fileType, documentType, null, null, null));
    }

    /**
     * 编辑模式 Token(支持修改+回调)
     */
    public String generateEditToken(String docUrl, String docTitle, String fileType, String documentType,
                                   String callbackUrl, String userName, String userId) throws Exception {
        validateRequiredParams(
                new StringParam(docUrl, "文档URL"),
                new StringParam(docTitle, "文档标题"),
                new StringParam(fileType, "文件类型"),
                new StringParam(documentType, "文档类别"),
                new StringParam(callbackUrl, "回调地址"),
                new StringParam(userName, "用户名"),
                new StringParam(userId, "用户ID")
        );
        return generateJWT(createDocumentPayload("edit", docUrl, docTitle, fileType, documentType, callbackUrl, userName, userId));
    }

    /**
     * 文件转换 Token(支持 docx→pdf 等格式转换)
     */
    public String generateConvertToken(String docUrl, String fileType, String outputType, String convertKey, String title) throws Exception {
        validateRequiredParams(
                new StringParam(docUrl, "文档URL"),
                new StringParam(fileType, "源文件类型"),
                new StringParam(outputType, "目标文件类型"),
                new StringParam(convertKey, "转换标识Key"),
                new StringParam(title, "文件标题")
        );
        return generateJWT(createConvertPayload(docUrl, fileType, outputType, convertKey, title));
    }

    /**
     * 通用 JWT 生成逻辑(优化 Base64 编码、签名算法稳定性)
     */
    private String generateJWT(Map<String, Object> payload) throws Exception {
        // 构建 Header
        Map<String, String> header = new HashMap<>(2);
        header.put("alg", ALGORITHM);
        header.put("typ", TYPE);

        // JSON 序列化(避免空值导致的解析异常)
        String headerJson = MAPPER.writeValueAsString(header);
        String payloadJson = MAPPER.writeValueAsString(payload);

        // Base64URL 编码(严格遵循 JWT 标准)
        String headerBase64 = base64UrlEncode(headerJson.getBytes(StandardCharsets.UTF_8));
        String payloadBase64 = base64UrlEncode(payloadJson.getBytes(StandardCharsets.UTF_8));

        // 生成签名(避免密钥为空)
        if (JWT_SECRET == null || JWT_SECRET.trim().isEmpty()) {
            throw new IllegalArgumentException("JWT_SECRET 不能为空");
        }
        String signature = hmacSha256(headerBase64 + ".", JWT_SECRET);

        return headerBase64 + "." + payloadBase64 + "." + signature;
    }

    /**
     * 构建文档操作 Payload(预览/编辑)
     */
    private Map<String, Object> createDocumentPayload(String mode, String docUrl, String docTitle, String fileType,
                                                     String documentType, String callbackUrl, String userName, String userId) {
        Map<String, Object> payload = new HashMap<>();
        payload.put("document", createDocumentInfo(docUrl, docTitle, fileType));
        payload.put("editorConfig", createEditorConfig(mode, callbackUrl, userName, userId));
        payload.put("documentType", documentType);

        // 设置过期时间(避免 Token 永久有效)
        long currentTime = System.currentTimeMillis() / 1000;
        payload.put("iat", currentTime);
        payload.put("exp", currentTime + EXPIRATION_SECONDS);
        return payload;
    }

    /**
     * 构建文件转换 Payload
     */
    private Map<String, Object> createConvertPayload(String docUrl, String fileType, String outputType, String convertKey, String title) {
        Map<String, Object> payload = new HashMap<>();
        payload.put("async", false); // 同步转换(立即返回结果)
        payload.put("filetype", fileType);
        payload.put("key", convertKey);
        payload.put("outputtype", outputType);
        payload.put("title", title);
        payload.put("url", docUrl);

        // 设置过期时间
        long currentTime = System.currentTimeMillis() / 1000;
        payload.put("iat", currentTime);
        payload.put("exp", currentTime + EXPIRATION_SECONDS);
        return payload;
    }

    /**
     * 构建文档基础信息(优化文档 Key 生成逻辑)
     */
    private Map<String, Object> createDocumentInfo(String docUrl, String docTitle, String fileType) {
        Map<String, Object> document = new HashMap<>();
        // 文档 Key:基于 URL + 时间戳生成(避免同一文档多次打开冲突)
        String docKey = Base64.getUrlEncoder().withoutPadding().encodeToString((docUrl + System.currentTimeMillis()).getBytes());
        document.put("fileType", fileType);
        document.put("key", docKey);
        document.put("title", docTitle);
        document.put("url", docUrl);
        return document;
    }

    /**
     * 构建编辑器配置
     */
    private Map<String, Object> createEditorConfig(String mode, String callbackUrl, String userName, String userId) {
        Map<String, Object> editorConfig = new HashMap<>();
        editorConfig.put("mode", mode);
        editorConfig.put("lang", DEFAULT_LANG); // 默认中文

        // 编辑模式额外配置
        if ("edit".equals(mode)) {
            editorConfig.put("callbackUrl", callbackUrl);
            editorConfig.put("canDownload", true); // 允许下载文档
            editorConfig.put("canPrint", true); // 允许打印文档

            // 用户信息
            Map<String, Object> user = new HashMap<>();
            user.put("name", userName);
            user.put("id", userId);
            editorConfig.put("user", user);
        }
        return editorConfig;
    }

    /**
     * 基础工具类:Base64URL 编码(遵循 JWT 标准,去除 = 填充)
     */
    private String base64UrlEncode(byte[] data) {
        return Base64.getUrlEncoder().withoutPadding().encodeToString(data);
    }

    /**
     * 基础工具类:HMAC-SHA256 签名
     */
    private String hmacSha256(String data, String secret) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
        mac.init(secretKeySpec);
        byte[] signatureBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
        return base64UrlEncode(signatureBytes);
    }

    /**
     * 参数校验工具(支持批量校验,清晰提示缺失参数)
     */
    private static class StringParam {
        private final String value;
        private final String name;
        public StringParam(String value, String name) {
            this.value = value;
            this.name = name;
        }
        public boolean isInvalid() { return value == null || value.trim().isEmpty(); }
        public String getName() { return name; }
    }

    private void validateRequiredParams(StringParam... params) {
        StringBuilder errorMsg = new StringBuilder();
        for (StringParam param : params) {
            if (param.isInvalid()) {
                errorMsg.append(param.getName()).append("、");
            }
        }
        if (errorMsg.length() > 0) {
            throw new IllegalArgumentException("必填参数缺失:" + errorMsg.substring(0, errorMsg.length() - 1));
        }
    }
}

优化点

  1. 采用单例模式,减少对象创建开销
  2. 新增集中式配置常量,便于维护
  3. 优化参数校验逻辑,错误提示更清晰
  4. 改进文档 Key 生成规则,避免冲突
  5. 编辑模式新增下载/打印权限配置
  6. 完善异常处理,便于问题排查
  7. 严格遵循 JWT 标准,提升兼容性

三、前端测试页优化(兼容性+用户体验)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>OnlyOffice 文档编辑测试</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body, html { height: 100%; overflow: hidden; }
        #placeholder { height: 100vh; width: 100vw; }
        /* 加载提示样式 */
        .loading {
            position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
            font-size: 18px; color: #333; z-index: 9999;
        }
    </style>
</head>
<body>
    <div class="loading">文档加载中...</div>
    <div id="placeholder"></div>

    <!-- 优先加载本地 OnlyOffice API(避免跨域/网络问题) -->
    <script type="text/javascript" src="http://localhost:1230/web-apps/apps/api/documents/api.js"></script>
    <script type="text/javascript">
        // 配置参数(替换为实际 Token 和文档地址)
        const config = {
            token: "替换为生成的编辑 Token",
            document: {
                fileType: "docx",
                title: "工作需要1.0.docx",
                url: "http://101.42.40.19:9000/xingyue-knowledge/工作需要1.0.docx"
            },
            documentType: "word",
            editorConfig: {
                callbackUrl: "https://xingyue.asia/test/callback",
                lang: "zh-CN",
                user: { name: "测试用户", id: "user123" },
                canDownload: true,
                canPrint: true
            },
            width: "100%",
            height: "100%",
            // 错误处理回调(提升用户体验)
            events: {
                onError: function(error) {
                    console.error("OnlyOffice 错误:", error);
                    alert(`文档加载失败:${error.message}\n请检查 Token 有效性和文档地址是否可访问`);
                },
                onReady: function() {
                    document.querySelector(".loading").style.display = "none"; // 隐藏加载提示
                }
            }
        };

        // 确保 API 加载完成后初始化
        if (window.DocsAPI) {
            initEditor();
        } else {
            // 降级处理:API 加载失败时重试
            setTimeout(initEditor, 1000);
        }

        function initEditor() {
            try {
                window.docEditor = new DocsAPI.DocEditor("placeholder", config);
            } catch (e) {
                console.error("编辑器初始化失败:", e);
                alert("编辑器初始化失败,请刷新页面重试");
            }
        }
    </script>
</body>
</html>

优化点

  1. 加载本地 API 脚本,避免跨域和网络依赖
  2. 新增加载提示,提升用户体验
  3. 增加错误处理回调,便于排查问题
  4. 优化样式适配,支持全屏显示
  5. 增加 API 加载重试机制,提升稳定性

四、文件转换接口优化(安全性+可读性)

1. 转换请求(推荐用 POSTMAN/ curl 测试)

请求地址http://localhost:1230/converter(注意:OnlyOffice 转换接口默认 80 端口,对应部署的 1230 端口)
请求方法 :POST
请求头

复制代码
Content-Type: application/json

请求体

json 复制代码
{
  "async": false,
  "filetype": "docx",
  "key": "convert_1762394085",
  "outputtype": "pdf",
  "title": "工作需要1.0.pdf",
  "url": "http://101.42.40.19:9000/xingyue-knowledge/工作需要1.0.docx",
  "token": "替换为生成的转换 Token"
}
相关推荐
妮妮喔妮2 小时前
root@lll:/data# sudo docker compose up -d 输入这个命令 控制台一直没有任何的反应 我需要如何排查呢?
运维·docker·容器·wsl docker
想唱rap3 小时前
Linux开发工具(4)
linux·运维·服务器·开发语言·算法
weixin_537765803 小时前
【Nginx优化】性能调优与安全配置
运维·nginx·安全
Lisonseekpan4 小时前
为什么国内禁用docker呢?
运维·docker·容器
扣脚大汉在网络4 小时前
如何在centos 中运行arm64程序
linux·运维·centos
R-G-B6 小时前
【P1】win10安装 Docker教程
运维·docker·容器
爱莉希雅&&&6 小时前
DNS分离解析案例
运维·网络·dns
Y淑滢潇潇6 小时前
RHCE Day2 时间管理服务器 NFS服务器
linux·运维·服务器
半熟的皮皮虾7 小时前
因需写了个内网运维专用的IP地址管理工具,有点不同
运维·服务器·tcp/ip