如何将在线文档编辑器集成至 Confluence管理平台

通过阅读本文,了解如何为 Confluence 与 ONLYOFFICE 创建集成插件以在 Confluence 中添加 DOCX、XLSX 与 PPTX 编辑功能

在线文档编辑功能可作为数字化协同工作区的宝贵补充,为此您还可使用开源工具。在本文中,我们将向您展示如何通过集成 ONLYOFFICE 文档来在 Confluence(Atlassian 推出的网页端企业 Wiki)中引入文档编辑功能

我们将创建一个集成应用程序(插件)来发挥沟通 Confluence 与 ONLYOFFICE 的桥梁作用。在其帮助下,用户将可在 Confluence 中编辑 DOCX、XLSX、PPTX 和其他办公文件

安装 ONLYOFFICE 文档

在此过程中,我们将需要可正常工作的 ONLYOFFICE 文档实例。其已被打包为了文档服务器,所以我们将在本文中使用此说法

您可使用 Docker 来进行安装,或选择其他安装选项

docker run -i -t -d -p 8080:80 onlyoffice/documentserver

本行代码将安装免费社区版。其中包含文档、电子表格与演示文稿编辑器,此外还有集成示例(一个简单的文档管理系统),其中演示了集成的各种功能,允许您在将编辑器集成至应用之前对其进行测试。

集成测试示例使用 Node.js 进行构建。您可查看其他语言的选项,包括 Java 测试示例。

现在,我们还是回到创建将编辑器连接至 Confluence 的插件上来。

使用 Atlassian SDK

在进行开发工作之前,我们需要安装 Atlassian SDK。基本上来说,这一 SDK 就是 Maven 的封装器。您可借助它来创建插件主体,然后在运行中的 Atlassian 应用程序中对其进行测试。

如需进行安装,可参考 WindowsLinux 或 Mac 的官方安装指南

之后我们将为 Confluence 创建一个插件主体。打开命令行并运行 atlas-create-confluence-plugin。在创建简单插件前,这一工具将要求您提供一些细节

您也可以在 IDE 中打开插件。部分 IDE 可能需要进行额外配置

虽然在对 VSCode 进行配置方面没有太多说明,但相关工作非常简单:您只需将其指向 SDK 自带的 Maven 即可。在 .vscode 文件夹中创建 settings.json 并填入以下内容:

{

"maven.executable.path": "C:\\Applications\\Atlassian\\atlassian-plugin-sdk-8.0.16\\apache-maven-3.5.4\\bin\\mvn\",

"java.configuration.maven.userSettings": "C:\\Applications\\Atlassian\\atlassian-plugin-sdk-8.0.16\\apache-maven-3.5.4\\conf\\settings.xml"

}

还要注意的是,具体路径可能会因为 SDK 的安装路径不同而有所区别。

使用 UI

借助 Atlassian 应用的 UI,您可实现很多功能,但是现在我们只需创建一个编辑按钮和用于加载编辑器的页面即可。

添加按钮非常简单。实际上,您甚至都不需要编写代码(除非按钮背后还有额外的逻辑)。基本上而言,Atlassian 应用会使用一个 XML 文件来声明插件的内容。文件位于:src\main\resources\atlassian-plugin.xml。此外还有助于让我们了解应用会对哪些 Java 类进行实例化。

(小提醒:我们将在代码中排除所有 import 行以节省空间)

如需添加按钮,我们只需添加这些代码即可:

<web-item key="onlyoffice-doceditor" name="Link for the attachment editing" section="system.attachment" weight="9">
    condition class="onlyoffice.IsOfficeFileAttachment">
        <param name="forEdit">;true</param>;
    </condition>
    <description>The link and the text for it to open the document which is available for editing.</description>
    <label key="onlyoffice.connector.editlink"/>
    <link><![CDATA[/plugins/servlet/onlyoffice/doceditor?attachmentId=$attachment.id]]></link>
    <styleClass>onlyoffice-doceditor</styleClass>
</web-item>

下面就来看看上面几行代码中的参数。为此我们也准备了一份详尽的官方文档,记得去看看。

大部分参数都无需过多解释。section 属性用于声明我们希望建立链接的位置<condition> 元素用于声明对展示链接的附件进行过滤的 Java 类。

基本上而言,这里的条件将用于检查用户的访问权限、最大文件大小以及文件扩展名。如果您对于其工作原理比较感兴趣,可在这里找到相关代码。

当然,我们需要添加的不仅仅是编辑按钮。此外还有用于查看文档和将其转换为不同格式的按钮,但主要的流程还是相同的。

接着我们来看看 <link> 元素。这里使用附件 id 参数将其导向 Servlet(链接如下:confluence/servlet/attachment_Id)。Servlet 将对用户请求进行处理并返回需要在编辑器中进行加载的页面。

Servlet 本质上是继承自  javax.servlet.http.HttpServlet 的类,但需对其 doGet 方法进行重写。这里我们还是保持简洁,暂时不去深究实现细节。

public class OnlyOfficeEditorServlet extends HttpServlet {

    private static final Logger log = LogManager.getLogger("onlyoffice.OnlyOfficeEditorServlet");

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        return VelocityUtils.getRenderedTemplate("templates/editor.vm", defaults);

    }

}

我们要做的唯一一件事是渲染一个 Velocity 模板。所以:src\main\resources\templates\editor.vm

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN” "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 

<html xmlns="http://www.w3.org/1999/xhtml\>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=ANSI" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width" />
    <title>${docTitle} - ONLYOFFICE</title>
    <link rel="icon" href="/favicon.ico" type="image/x-icon" /> 
    <style type="text/css"> 
        html { height: 100%; width: 100%; }
        body { 
                background: #fff; color: #333; font-family: Arial, Tahoma, sans-serif;
                font-size: 12px; font-weight: normal; text-decoration: none;
                height: 100%; margin: 0; overflow-y: hidden; padding: 0;
        }
        .form { height: 100%; }
        div { margin: 0; padding: 0; } 
    </style>
</head>

<body>

    <div class="form">
           <div id="iframeEditor"></div>
    </div>
</body>
</html> 

在模板中将有一个 JavaScript 脚本用于在编辑器中加载页面。您可能会注意到这里有多个 `${variables}`,我们会在稍后提供相关信息。为了让 Servlet 能够正常工作,我们需要让 Atlassian 知晓其存在。所以,这里我们需要对于 atlassian-plugin.xml. 进行一些修改。

<servlet key="OnlyOfficeDocEditor" class="onlyoffice.OnlyOfficeEditorServlet" name="Document Editor">

    <description>The fully functional editor for most known formats of text documents, spreadsheets and presentations used to open these types of documents for editing or preview.</description>

    <url-pattern>/onlyoffice/doceditor</url-pattern>

</servlet>

Servlet 和插件配置

此时,我们马上就能去打开文档进行查看了。现在我们还需要为页面模板提供变量。其中一个将包含指向文档服务器的 URL,这里最好不要直接写死在代码中。幸运的是,我们可以借助一个很棒的接口来对插件设置项与 UI 进行操作

下面我们就来创建另一个配置 Servlet(别忘了将其添加至 atlassian-plugin.xml 中)。

public class OnlyOfficeConfServlet extends HttpServlet {

    @ComponentImport
    private final UserManager userManager;

    @ComponentImport
    private final PluginSettingsFactory pluginSettingsFactory;

    @Inject
    public OnlyOfficeConfServlet(UserManager userManager, PluginSettingsFactory pluginSettingsFactory) {
        this.userManager = userManager;
        this.pluginSettingsFactory = pluginSettingsFactory;
    }

    private static final Logger log = LogManager.getLogger("onlyoffice.OnlyOfficeConfServlet");

    private static final long serialVersionUID = 1L;

    private String AppendSlash(String str) {
        if (str == null || str.isEmpty() || str.endsWith("/"))
            return str;
        return str + "/";
    }

    private String getBody(InputStream stream) {
        Scanner scanner = null;
        Scanner scannerUseDelimiter = null;
      
        try {
            scanner = new Scanner(stream);
            scannerUseDelimiter = scanner.useDelimiter("\\A");
            return scanner.hasNext() ? scanner.next() : "";
        } finally {
            scannerUseDelimiter.close();
            scanner.close();
        }
    }
}

这里我们有两种实用方法,代码本身已足够简单明了。我们还需要另外两方法。

第一个是 doGet。用于获取当前设置并将其提供给模板。

@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    String username = userManager.getRemoteUsername(request);

    if (username == null || !userManager.isSystemAdmin(username)) {
        SettingsManager settingsManager = (SettingsManager) ContainerManager.getComponent("settingsManager");
        String baseUrl = settingsManager.getGlobalSettings().getBaseUrl();
        response.sendRedirect(baseUrl);
        return;
    }

    PluginSettings pluginSettings = pluginSettingsFactory.createGlobalSettings();
    String apiUrl = (String) pluginSettings.get("onlyoffice.apiUrl");

    response.setContentType("text/html;charset=UTF-8");
    PrintWriter writer = response.getWriter();

    Map<String, Object> contextMap = MacroUtils.defaultVelocityContext();

    contextMap.put("docserviceApiUrl", apiUrl);
    writer.write(getTemplate(contextMap));
}

private String getTemplate(Map<String, Object> map) throws UnsupportedEncodingException {
    return VelocityUtils.getRenderedTemplate("templates/configure.vm", map);
}

第二个是 doPost。该方法将接收 JSON 对象,对其进行解析并覆盖当前设置。

@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    String username = userManager.getRemoteUsername(request);
    if (username == null || !userManager.isSystemAdmin(username)) {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        return;
    }

    String body = getBody(request.getInputStream());
    if (body.isEmpty()) {
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        return;
    }

    String apiUrl;
    try {
        JSONObject jsonObj = new JSONObject(body);
        apiUrl = AppendSlash(jsonObj.getString("apiUrl"));
    } catch (Exception ex) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        ex.printStackTrace(pw);
        String error = ex.toString() + "\n" + sw.toString();
        log.error(error);

        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        response.getWriter().write("{\"success\": false, \"message\": \"jsonparse\"}");
        return;
    }

    PluginSettings pluginSettings = pluginSettingsFactory.createGlobalSettings();
    pluginSettings.put("onlyoffice.apiUrl", apiUrl);

    response.getWriter().write("{\"success\": true}");
}

在这两种方法中,我们还会检查用户是否有权访问这些设置。实际上,我们还会检查与文档服务器之间的连接,以便对潜在问题进行识别。完整代码可在此处查看。

我们还能在 Atlassian 插件管理页面中添加一个配置按钮。只需将此行代码添加至 atlassian-plugin.xml 中的 <plugin-info> 元素内即可

<param name="configure.url">/plugins/servlet/onlyoffice/configure</param>

对了,别忘记 Velocity 模板。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 

<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>ONLYOFFICE</title>
        <meta name="decorator" content="atl.admin" />
    </head>

    <body>
        <div id="onlyofficeMsg"></div>
        <form id="onlyofficeConf" class="aui top-label">          
            <h3>$i18n.getText('onlyoffice.configuration.doc-section')</h3>
            <div class="field-group">
                <label for="apiUrlField">$i18n.getText('onlyoffice.configuration.doc-url')</label>
                <input type="text" id="apiUrlField" value="${docserviceApiUrl}" name="apiUrlField" class="text onlyoffice-tooltip" title="$i18n.getText('onlyoffice.configuration.doc-url-tooltip')">
            </div>
            <div class="field-group">
                <input id="onlyofficeSubmitBtn" type="submit" value="$i18n.getText('onlyoffice.configuration.save')" class="button">
            </div>
        </form>
    </body>
</html> 

下面让我们返回编辑器 Servlet 并对其进行修改。

public class OnlyOfficeEditorServlet extends HttpServlet {
    @ComponentImport
    private final LocaleManager localeManager;
    private final UrlManager urlManager;

    @Inject
    public OnlyOfficeEditorServlet(LocaleManager localeManager, UrlManager urlManager) {        
        this.urlManager = urlManager;
        this.localeManager = localeManager;
    }

    private static final Logger log = LogManager.getLogger("onlyoffice.OnlyOfficeEditorServlet");
    private static final long serialVersionUID = 1L;

    private Properties properties;
    
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        if (!AuthContext.checkUserAuthorisation(request, response)) {
            return;
        }
    
        String apiUrl = urlManager.getPublicDocEditorUrl();
        if (apiUrl == null || apiUrl.isEmpty()) {
        apiUrl = "";
        }

        ConfigurationManager configurationManager = new ConfigurationManager();
        properties = configurationManager.GetProperties();

        String fileUrl = "";
        String key = "";
        String fileName = "";
        String errorMessage = "";
        ConfluenceUser user = null;

        String attachmentIdString = request.getParameter("attachmentId");
        Long attachmentId;

        try {
            attachmentId = Long.parseLong(attachmentIdString);
            log.info("attachmentId " + attachmentId);

            user = AuthenticatedUserThreadLocal.get();
            log.info("user " + user);
            if (AttachmentUtil.checkAccess(attachmentId, user, false)) {
                key = DocumentManager.getKeyOfFile(attachmentId);

                fileName = AttachmentUtil.getFileName(attachmentId);

                fileUrl = urlManager.GetFileUri(attachmentId);
            } else {
                log.error("access deny");
                errorMessage = "You don not have enough permission to view the file";
            }
        } catch (Exception ex) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            ex.printStackTrace(pw);
            String error = ex.toString() + "\n" + sw.toString();
            log.error(error);
            errorMessage = ex.toString();
        }
        
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter writer = response.getWriter();

        writer.write(getTemplate(apiUrl, fileUrl, key, fileName, user, errorMessage));
    }

private String getTemplate(String apiUrl, String fileUrl, String key, String fileName,
        ConfluenceUser user, String errorMessage) throws UnsupportedEncodingException {
    Map<String, Object> defaults = MacroUtils.defaultVelocityContext();
    Map<String, String> config = new HashMap<String, String>();

    String docTitle = fileName.trim();
    String docExt = docTitle.substring(docTitle.lastIndexOf(".") + 1).trim().toLowerCase(); 

    config.put("docserviceApiUrl", apiUrl + properties.getProperty("files.docservice.url.api"));
    config.put("errorMessage\", errorMessage);
    config.put("docTitle", docTitle);

    JSONObject responseJson = new JSONObject();
    JSONObject documentObject = new JSONObject();
    JSONObject editorConfigObject = new JSONObject();
    JSONObject userObject = new JSONObject();
    JSONObject permObject = new JSONObject();

    try {
        responseJson.put("type", "desktop");
        responseJson.put("width", "100%");
        responseJson.put("height", "100%");
        responseJson.put("documentType", getDocType(docExt));

        responseJson.put("document", documentObject);
        documentObject.put("title", docTitle);
        documentObject.put("url", fileUrl);
        documentObject.put("fileType", docExt);
        documentObject.put("key", key);
        documentObject.put("permissions", permObject);
       permObject.put("edit", false);

        responseJson.put("editorConfig", editorConfigObject);
        editorConfigObject.put("lang", localeManager.getLocale(user).toLanguageTag());
        editorConfigObject.put("mode", "edit");

        if (user != null) {
            editorConfigObject.put("user", userObject);
            userObject.put("id", user.getName());
            userObject.put("name", user.getFullName());
        }

            // AsHtml at the end disables automatic html encoding             
            config.put("jsonAsHtml", responseJson.toString());
    } catch (Exception ex) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            ex.printStackTrace(pw);
            String error = ex.toString() + "\n" + sw.toString();
            log.error(error);
        }

        defaults.putAll(config);
        return VelocityUtils.getRenderedTemplate("templates/editor.vm", defaults);
    }

    private String getDocType(String ext) {
        if (".doc.docx.docm.dot.dotx.dotm.odt.fodt.ott.rtf.txt.html.htm.mht.pdf.djvu.fb2.epub.xps".indexOf(ext) != -1)
            return \"text\";
        if (".xls.xlsx.xlsm.xlt.xltx.xltm.ods.fods.ots.csv".indexOf(ext) != -1)
return "spreadsheet";
        if (".pps.ppsx.ppsm.ppt.pptx.pptm.pot.potx.potm.odp.fodp.otp".indexOf(ext) != -1)
            return "presentation";
        return null;
    }
} 

这将构建一个在页面上打开编辑器时所需的 JSON 对象。请注意,我们会将 JSON 对象放入结尾是 AsHtml 的变量中。其会禁用自动 HTML 编码,所以我们可将其放入模板的 <script> 标记内,如下:

var json = '${jsonAsHtml}';

这样就能正常工作了。

我们还使用了三个工具类:AttachmentUtilUrlManager 以及 DocumentManager。这些类有点超出本文所涵盖的范围(除了 AttachmentUtilDocumentManager 中的一部分内容,但我们稍后也会提到),所以如果您对此感兴趣的话,可自行进行探索。

最后还有一件事需要我们去处理:将文档提供给文档服务器。

这里我们再次创建一个 Servlet,其中唯一的方法就是提供文件。

public class OnlyOfficeSaveFileServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
    private static final Logger log = LogManager.getLogger("onlyoffice.OnlyOfficeSaveFileServlet");

    @ComponentImport
    private final PluginSettingsFactory pluginSettingsFactory;

    private final PluginSettings settings;

    @Inject
    public OnlyOfficeSaveFileServlet(PluginSettingsFactory pluginSettingsFactory, JwtManager jwtManager) {
        this.pluginSettingsFactory = pluginSettingsFactory;
        settings = pluginSettingsFactory.createGlobalSettings();
    }

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String vkey = request.getParameter("vkey");
        log.info("vkey = " + vkey);
        String attachmentIdString = DocumentManager.ReadHash(vkey);

        Long attachmentId = Long.parseLong(attachmentIdString);
        log.info("attachmentId " + attachmentId);

        String contentType = AttachmentUtil.getMediaType(attachmentId);
        response.setContentType(contentType);

        InputStream inputStream = AttachmentUtil.getAttachmentData(attachmentId);
        response.setContentLength(inputStream.available());

        byte[] buffer = new byte[10240];

        OutputStream output = response.getOutputStream();
        for (int length = 0; (length = inputStream.read(buffer)) > 0;) {
            output.write(buffer, 0, length);
        }
    }
}

运行与测试

此时,一切准备工作都已就绪,我们可以对附件进行打开并查看。

运行 atlas-run 命令以构建插件,然后运行 Confluence。

或者,您也可以运行 atlas-package 来仅构建一个 .jar

无论选择哪种方式,您都应该前往“设置 -> 管理应用”中上传插件并点击配置。指定文档服务器 URL 并测试插件。

编辑与共同编辑

如需对文档进行编辑,我们就还需要进行一些更改。首先来看看其工作原理。

这里有个名为 callbackUrl 的参数。其应该指向一个从文档服务器接收 JSON 数据的 Servlet,用于描述文档编辑的状态。在用户关闭文档时,其会发出一则消息告知编辑完成,还会发送更新后文档的 URL,便于我们进行下载。

您可能也会想了解一下共同编辑的原理。只要 key 参数相同,用户就可以对同一个文档进行编辑。在为文件生成 key 时有两件重要的事需要考虑。

1. 每个文档的 key 都不应相同。

2. 文档中出现变更时 key 也应当变更。

通常而言,ID + 修改日期的组合能够胜任这一工作。但也有其不适用的情况。如果您打算实现强制保存功能,那么就需要生成一个在编辑全程保持相同的 key。

key 的生成方法位于 AttachmentUtilDocumentManager 中。

让我们将下面的代码添加至编辑器 Servlet 中,就放在 fileUrl = urlManager.GetFileUri(attachmentId); 代码行后方即可

if (AttachmentUtil.checkAccess(attachmentId, user, true)) {
    callbackUrl = urlManager.getCallbackUrl(attachmentId);
}

将其传递给 getTemplate 函数

writer.write(getTemplate(apiUrl, callbackUrl, fileUrl, key, fileName, user, errorMessage));

然后在此处使用

permObject.put("edit", callbackUrl != null && !callbackUrl.isEmpty());

...

editorConfigObject.put("callbackUrl", callbackUrl);

接着我们需要创建一个回调处理器。我们来修改一下用于提供文件的 OnlyOfficeSaveFileServlet。这一过程非常简单。我们将对传入的 JSON 对象进行解析,查看其中的 status 并在有需要时保存文档。相关文档可在此处查看。

@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    response.setContentType("text/plain; charset=utf-8");

    String vkey = request.getParameter("vkey");
    log.info("vkey = " + vkey);
    String attachmentIdString = DocumentManager.ReadHash(vkey);

    String error = "";
    try {
        processData(attachmentIdString, request);
    } catch (Exception e) {
        error = e.getMessage();
    }

    PrintWriter writer = response.getWriter();
    if (error.isEmpty()) {
        writer.write("{\"error\":0}");
    } else {
        response.setStatus(500);
        writer.write("{\"error\":1,\"message\":\"" + error + "\"}");
    }

    log.info("error = " + error);
}

private void processData(String attachmentIdString, HttpServletRequest request) throws Exception {
    log.info("attachmentId = " + attachmentIdString);
    InputStream requestStream = request.getInputStream();
    if (attachmentIdString.isEmpty()) {
        throw new IllegalArgumentException("attachmentId is empty");
    }

    HttpURLConnection connection = null;
    try {
        Long attachmentId = Long.parseLong(attachmentIdString);

        String body = getBody(requestStream);
        log.info("body = " + body);
        if (body.isEmpty()) {
            throw new IllegalArgumentException("requestBody is empty");
        }

        JSONObject jsonObj = new JSONObject(body);

        long status = jsonObj.getLong("status");
        log.info("status = " + status);

        // MustSave, Corrupted
        if (status == 2 || status == 3) {
            ConfluenceUser user = null;
            JSONArray users = jsonObj.getJSONArray("users");
            if (users.length() > 0) {
                String userName = users.getString(0);

                UserAccessor userAccessor = (UserAccessor) ContainerManager.getComponent("userAccessor");
                user = userAccessor.getUserByName(userName);
                log.info("user = " + user);
            }

            if (user == null || !AttachmentUtil.checkAccess(attachmentId, user, true)) {
                throw new SecurityException("Try save without access: " + user);
            }

            String downloadUrl = jsonObj.getString("url");
            log.info("downloadUri = " + downloadUrl);

            URL url = new URL(downloadUrl);

            connection = (HttpURLConnection) url.openConnection();
            int size = connection.getContentLength();
            log.info("size = " + size);

            InputStream stream = connection.getInputStream();

            AttachmentUtil.saveAttachment(attachmentId, stream, size, user);
        }
    } catch (Exception ex) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        ex.printStackTrace(pw);
        String error = ex.toString() + "\n" + sw.toString();
        log.error(error);

        throw ex;
    } finally {
        if (connection != null) {
            connection.disconnect();
        }
    }
}
private String getBody(InputStream stream) {
    Scanner scanner = null;
    Scanner scannerUseDelimiter = null;
    try {
        scanner = new Scanner(stream);
        scannerUseDelimiter = scanner.useDelimiter("\\A");
        return scanner.hasNext() ? scanner.next() : "";
    } finally {
        scannerUseDelimiter.close();
        scanner.close();
    }
}

好了,这 OK 啦。接下来就是对项目进行重新编译与测试了。

安全性

您可能想知道,我们如何确保通过 POST 方式传递至 Servlet 的 JSON 确实来自文档服务器。答案很简单:文档服务器使用 JWT 来实现这一功能。JWT 是很热门的话题,也是值得单独用一些篇幅来进行介绍的话题,这里我们就不展开了。

我们所使用的是 JwtManager 类。基本上而言,JWT 是基于密钥的哈希加密 JSON。

首先我们需要为密钥添加一个新的设置。这应该不难。您可在这里找到代码:configure.vm 模板配置 Servlet

然后我们会向编辑器 Servlet 构造函数中添加 JwtManager,并在 config.put("jsonAsHtml", responseJson.toString());  代码行之前对其进行使用:

if (jwtManager.jwtEnabled()) {
    responseJson.put("token", jwtManager.createToken(responseJson));
}

现在我们就取得了文档服务器的信任

为了确保文档服务器也能得到我们的信任,我们将在处理回调时使用 JWT。

文档服务器传递 JWT 的方式有两种:通过 HTTP 标头传递,或包含在 JSON 中传递,具体取决于配置情况。这里我们将同时对两者进行了解。

下面来修改一下回调 Servlet,在其构造函数中添加 JwtManager,并在 JSONObject jsonObj = new JSONObject(body); 代码行后使用:

if (jwtManager.jwtEnabled()) {
    String token = jsonObj.optString("token");
    Boolean inBody = true;

    if (token == null || token == "") {
        String jwth = (String) settings.get("onlyoffice.jwtHeader");
        String header = (String) request.getHeader(jwth == null || jwth.isEmpty() ? "Authorization" : jwth);
        token = (header != null && header.startsWith("Bearer ")) ? header.substring(7) : header;
        inBody = false;
    }

    if (token == null || token == "") {
        throw new SecurityException("Try save without JWT");
    }

    if (!jwtManager.verify(token)) {
        throw new SecurityException("Try save with wrong JWT");
    }

    JSONObject bodyFromToken = new JSONObject(
            new String(Base64.getUrlDecoder().decode(token.split("\\.")[1]), "UTF-8"));

    if (inBody) {
        jsonObj = bodyFromToken;
    } else {
        jsonObj = bodyFromToken.getJSONObject("payload");
    }
}

好了,收工!现在我们就能在 JWT 的保护下查看和编辑文档了,其将不会受到未授权访问的侵扰,此外还有便捷的配置项可供使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值