JavaScript

31 mar, 2021

Como integrar os editores de documentos online com proteção JWT em seu aplicativo

Publicidade

Neste artigo, mostraremos a você como levar o recurso de edição de documentos para seu aplicativo da Web usando os editores online do ONLYOFFICE e também como proteger seus documentos contra acesso não autorizado usando o JSON Web Token.

Sobre os editores

O ONLYOFFICE é um pacote office open-source com:

· edição de documento de texto, planilha e apresentação,
· a maior compatibilidade com os formatos MS Office (docx, xlsx),
· colaboração em tempo real em documentos office.

Os editores podem ser integrados a quase todos os aplicativos da Web. Por padrão, você não recebe nenhum sistema de gestão de documentos com os
editores. Assim, para mostrar como a integração é feita, vamos integrá-los a um aplicativo de exemplo em Node.js (passo 1-3) e, a seguir, proteger a edição contra acesso indesejado (passo 4).

Passo 1. Criar o esqueleto do projeto

Presumimos que você já tenha o Node.js instalado. Caso contrário, você pode obtê-lo aqui.

Vamos criar uma pasta para o projeto, abri-la e executar o seguinte comando:

npm init

Será pedido para inserirmos metadados (nome do pacote, versão, licença). Você pode fazer isso ou simplesmente pular usando enter. Com esses dados, podemos criar o package.json.

Depois disso, instale o express:

npm install express --save

Precisamos do parâmetro –save para npm para indicar no arquivo package.json que os projetos dependem do pacote express.

Vamos criar esses arquivos:

  • index.js para iniciar e configurar o servidor express,
  • app/app.js para lógica de processamento de consulta,
  • app/config.json para parâmetros alteráveis, como portas, endereço de editores, etc. (no nosso caso, usaremos apenas um arquivo JSON, mas em um projeto real é melhor usar algo mais sólido).

O index.js deve conter este código:

const express = require('express');

const cfg = require('./app/config.json');

const app = express();

app.use(express.static("public"));

app.listen(cfg.port, () => {

    console.log(`Server is listening on ${cfg.port}`);

});

O config.js deve conter informações sobre a porta que você usará para os editores de documentos:

{"port": 8080}

Vamos criar uma pasta pública e adicionar um arquivo index.html simples a ela e, a seguir, adicionar essas linhas ao package.json:

"scripts": {

"start": "node index.js"

}

Agora é hora de executar nosso aplicativo para ter certeza de que ele está funcionando usando este comando:

npm start

Para verificar, precisamos abrir o navegador http://localhost:8080.

Passo 2. Visualizar documentos

Antes de integrar os editores, precisamos instalar o ONLYOFFICE Document Server. A forma mais fácil de fazer isso é usando o Docker. Se tiver o Docker instalado, você poderá obter os editores com um único comando:

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

O Document Server deve ser capaz de enviar solicitações http ao servidor e vice-versa. Vamos adicionar os editores (Document Server) e nossos endereços de aplicativo de exemplo ao config.json. No nosso caso, é assim:

"editors_root": "http://192.168.0.152:9090/" ,
"example_root": "http://192.168.0.152:8080/"

Nesta etapa, adicionaremos funções para trabalhar com arquivos (obtendo os arquivos, listas, seus nomes e extensões) para app/fileManager.js:

const fs = require('fs');

const path = require('path');

 

const folder = path.join(__dirname, "..", "public");

const emptyDocs = path.join(folder, "emptydocs");

 

function listFiles() {

    var files = fs.readdirSync(folder);

    var result = [];

    for (let i = 0; i < files.length; i++) {

        var stats = fs.lstatSync(path.join(folder, files[i]));

        if (!stats.isDirectory()) result.push(files[i])

    }

    return result;

}

 

function exists(fileName) {

    return fs.existsSync(path.join(folder, fileName));

}

 

function getDocType(fileName) {

    var ext = getFileExtension(fileName);

    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;

}

 

function isEditable(fileName) {

    var ext = getFileExtension(fileName);

    return ".docx.xlsx.pptx".indexOf(ext) != -1;

}

 

function createEmptyDoc(ext) {

    var fileName = "new." + ext;

    if (!fs.existsSync(path.join(emptyDocs, fileName))) return null;

    var destFileName = getCorrectName(fileName);

    fs.copyFileSync(path.join(emptyDocs, fileName), path.join(folder, destFileName));

    return destFileName;

}

 

function getCorrectName(fileName) {

    var baseName = getFileName(fileName, true);

    var ext = getFileExtension(fileName);

    var name = baseName + "." + ext;

    var index = 1;

 

    while (fs.existsSync(path.join(folder, name))) {

        name = baseName + " (" + index + ")." + ext;

        index++;

    }

 

    return name;

}

 

function getFileName(fileName, withoutExtension) {

    if (!fileName) return "";

 

    var parts = fileName.toLowerCase().split(path.sep);

    fileName = parts.pop();

 

    if (withoutExtension) {

        fileName = fileName.substring(0, fileName.lastIndexOf("."));

    }

 

    return fileName;

}

 

function getFileExtension(fileName) {

    if (!fileName) return null;

    var fileName = getFileName(fileName);

    var ext = fileName.toLowerCase().substring(fileName.lastIndexOf(".") + 1);

    return ext;

}

 

function getKey(fileName) {

    var stat = fs.statSync(path.join(folder, fileName));

    return new Buffer(fileName + stat.mtime.getTime()).toString("base64");

}

 

module.exports = {

    listFiles: listFiles,

    createEmptyDoc: createEmptyDoc,

    exists: exists,

    getDocType: getDocType,

    getFileExtension: getFileExtension,

    getKey: getKey,

    isEditable: isEditable

}

Vamos também adicionar o pacote pug.

npm install pug --save

A seguir, podemos excluir nosso index.html já que adicionamos o mecanismo de modelo pug. Agora, vamos criar a pasta de visualizações e adicionar esta linha ao index.js para conectar o mecanismo a ela.

app.set("view engine", "pug");

Agora, podemos criar views/index.pug e adicionar botões para criar e abrir documentos.

extends master.pug

 

block content

  div

    a(href="editors?new=docx", target="_blank")

      button= "Create DOCX"

    a(href="editors?new=xlsx", target="_blank")

      button= "Create XLSX"

    a(href="editors?new=pptx", target="_blank")

      button= "Create PPTX"

  div

    each val in files

      div

      a(href="editors?filename=" + val, target="_blank")= val

 

extends master.pug

 

block content

  div

    a(href="editors?new=docx", target="_blank")

      button= "Create DOCX"

    a(href="editors?new=xlsx", target="_blank")

      button= "Create XLSX"

    a(href="editors?new=pptx", target="_blank")

      button= "Create PPTX"

  div

    each val in files

      div

      a(href="editors?filename=" + val, target="_blank")= val

 

A lógica será descrita em app/app.js: nós criamos um arquivo (ou verificamos se ele já está presente no sistema de arquivos), e formamos a configuração do editor (leia como fazer isso corretamente aqui) e então retornamos ao modelo da página:

const fm = require('./fileManager');

const cfg = require('./config.json');

 

function index(req, res) {

    res.render('index', { title: "Index", files: fm.listFiles() });

}

 

function editors(req, res) {

    var fileName = "";

 

    if (req.query.new) {

        var ext = req.query.new;

        fileName = fm.createEmptyDoc(ext);

    } else if (req.query.filename) {

        fileName = req.query.filename;

    }

 

    if (!fileName || !fm.exists(fileName)) {

        res.write("can't open/create file");

        res.end();

        return;

    }

 

    res.render('editors', { title: fileName, api: cfg.editors_root, cfg: JSON.stringify(getEditorConfig(req, fileName)) });

}

 

function getEditorConfig(req, fileName) {

    var canEdit = fm.isEditable(fileName);

    return {

        width: "100%",

        height: "100%",

        type: "desktop",

        documentType: fm.getDocType(fileName),

        document: {

            title: fileName,

            url: cfg.example_root + fileName,

            fileType: fm.getFileExtension(fileName),

            key: fm.getKey(fileName),

            permissions: {

                download: true,

                edit: canEdit

            }

        },

        editorConfig: {

            mode: canEdit ? "edit" : "view",

            lang: "en"

        }

    }

}

 

module.exports = {

    index: index,

    editors: editors

};

Nesta página, carregamos o script de editores http://docserver/web-apps/apps/api/documents/api.js e adicionamos a instância de editores `new DocsAPI.DocEditor(“iframeEditor”, !{cfg})`.

Agora vamos executar o aplicativo e verificar tudo.

Passo 3. Editar documentos

Para editar um documento ou, para ser mais exato, para salvá-lo, você precisa processar uma solicitação do Document Server bem como especificar no arquivo de configuração o que fazer com essa solicitação. Você pode ler mais sobre as solicitações do Document Server aqui.

O Document Server envia a solicitação POST com conteúdo JSON. Por isso, precisamos conectar o middleware para analisar o JSON toindex.js.

app.use(express.json());

Para receber esse objeto em primeiro lugar, precisaremos informar ao Document Server onde fazer essa solicitação. Vamos adicionar callbackUrl: cfg.example_root + “callback?filename=” + fileName à configuração do editor.

A seguir, temos que criar uma função de retorno de chamada que obterá as informações do Document Server e verificará o status da solicitação.

function callback(req, res) {

    try {

        var fileName = req.query.filename;

 

        !checkJwtToken(req);

        var status = req.body.status;

 

        switch (status) {

            case 2:

            case 3:

                fm.downloadSave(req.body.url, fileName);

                break;

 

            default:

                // to-do: process other statuses

                break;

        }

    } catch (e) {

        res.status(500);

        res.write(JSON.stringify({ error: 1, message: e.message }));

        res.end();

        return;

    }

    res.write(JSON.stringify({ error: 0 }));

    res.end();

}

Neste exemplo, estamos olhando apenas para a solicitação de salvamento de arquivo. Assim que recebermos o sinal para salvar o arquivo, pegaremos o link para nosso documento dos dados POST e o salvaremos em nosso sistema de arquivos.

functiondownloadSave(downloadFrom, saveAs) {

http.get(downloadFrom, (res) => {

if (res.statusCode==200) {

varfile=fs.createWriteStream(path.join(folder, saveAs));

res.pipe(file);

file.on('finish', function() {

file.close();

           });

       }

   });

}

Agora temos um aplicativo de exemplo com recursos de edição. Vamos protegê-lo contra acesso indesejado usando JWT.

Passo 4. Implementar JWT

O ONLYOFFICE usa JSON Web Token para proteger a troca de dados entre os editores, seus serviços internos e o armazenamento. Ele solicita uma assinatura criptografada que é hospedada no token. Esse token valida o direito de realizar uma determinada operação com os dados.

Se estiver planejando usar o JWT, é melhor usar um pacote pronto, por exemplo, este. Mas desta vez faremos tudo sozinhos para entender como funciona.

Algumas teorias para começar.

O JWT consiste em três partes:

header – contém informações meta, por exemplo, um algoritmo de criptografia

payload – contém dados

hash – hash baseado nas duas partes anteriores e na chave secreta

Todas essas três partes são objetos JSON, enquanto o token JSON em si é um base64url de todas as partes conectadas por pontos.

É assim que tudo funciona:

  • O Server1 calcula um hash com base em uma chave e uma string de header.payload.
  • O token header.payload.hash é formado.
  • O Server2 recebe esse token, forma seu hash com base nas duas primeiras partes dele.
  • O Server2 compara o token gerado com o recebido; se eles corresponderem, os dados não foram falsificados.

Agora vamos implementar o JWT em nosso exemplo de integração.

Os editores permitem a transferência do JWT token no corpo e no cabeçalho da solicitação. O melhor é usar o corpo da solicitação, pois o tamanho do cabeçalho é limitado. Mas vamos considerar as duas variantes.

Se usar cabeçalhos para transferir o token, você precisará adicionar os dados ao objeto usando a chave de payload.

Se você transferir o token no corpo da solicitação, payload terá a seguinte aparência:

{

"key": "value"

}

E se você transferi-lo no cabeçalho:

{

"payload": {

"key": "value"

   }

}

Vamos adicionar a chave ao config.json:

"jwt_secret": "supersecretkey"

Para iniciar os editores com JWT, precisamos apenas especificar as variáveis ​​de ambiente:

docker run -i -t -d -p 9090:80 -e JWT_ENABLED=true -e JWT_SECRET=supersecretkey onlyoffice/documentserver

Se estiver planejando transferir o token no corpo da solicitação, adicione mais uma variável-e JWT_IN_BODY=true

docker run -i -t -d -p 9090:80 -e JWT_ENABLED=true -e JWT_SECRET=supersecretkey -e JWT_IN_BODY=true onlyoffice/documentserver

O app/jwtManager.js contém toda a lógica para trabalhar com o JWT.Precisamos apenas adicionar token a config quando os editores são abertos.

if (jwt.isEnabled()) {

editorConfig.token=jwt.create(editorConfig);

}

O token em si é formado pelo algoritmo acima na seção de teoria. Esta é a função:

function create(payloadObj) {

    if (!isEnabled()) return null;

 

    var headerObj = {

        alg: "HS256",

        typ: "JWT"

    };

 

    header = b64Encode(headerObj);

    payload = b64Encode(payloadObj);

    hash = calculateHash(header, payload);

 

    return header + "." + payload + "." + hash;

}

 

function calculateHash(header, payload) {

    return b64UrlEncode(crypto.createHmac("sha256", cfg.jwt_secret).update(header + "." + payload)

        .digest("base64"));

}

Ela nos permitirá abrir um documento, mas também devemos verificar o token recebido do próprio Document Server.

Verificaremos o corpo e o cabeçalho. A função é muito simples, ela gerará um erro se algo der errado. Caso contrário, ela verificará um token e mesclará o corpo da solicitação com o payload do token.

function checkJwtToken(req) {

    if (!jwt.isEnabled()) return;

    var token = req.body.token;

    var inBody = true;

 

    if (!token && req.headers.authorization) {

        token = req.headers.authorization.substr("Bearer ".length);

        inBody = false;

    }

 

    if (!token) throw new Error("Expected JWT token");

 

    var payload = jwt.verify(token);

 

    if (!payload) throw new Error("JWT token validation failed");

 

    if (inBody) {

        Object.assign(req.body, payload);

    } else {

        Object.assign(req.body, payload.payload);

    }

}

A função que faz a verificação também é muito simples.

function verify(token) {

    if (!isEnabled()) return null;

    if (!token) return null;

 

    var parts = token.split(".");

    if (parts.length != 3) {

        return null;

    }

 

    var hash = calculateHash(parts[0], parts[1]);

    if (hash !== parts[2]) return null;

    return b64Decode(parts[1]);

}

Vamos dar uma olhada nos métodos em jwtManager.

O método de criação obtém um objeto com dados, por exemplo:

{

"key": "value"

}

Constrói cabeçalho JWT

{

"alg": "HS256",

"typ": "JWT"

}

A seguir, o método pega os dois objetos, cria uma representação de string JSON e a codifica como base64url. Depois, ele conecta as duas linhas com pontos e forma um hash com base na sua chave. Neste exemplo, usaremos supersecretkey.

Como resultado, obtemos este token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ2YWx1ZSJ9.ozm44FMRAlWXB0PhJg935wyOkp7wtj1jXvgEGIS0iig

O método verify obtém o token, separa suas partes usando pontos, pega as primeiras duas partes e cria o hash. A seguir, ele compara o hash gerado com o recebido. Se corresponderem, o payload será decodificado, e o objeto JSON será retornado.

Você também pode testar com o token, ver como ele é construído e encontrar bibliotecas open-source para diferentes linguagens aqui.

Observe que esta é uma implementação mínima do JWT. O padrão é grande e tem todos os tipos de coisas a serem levadas em consideração, por exemplo, a vida limitada do token. Assim, recomendamos o uso de pacotes prontos para trabalhar com o JWT na produção.

Esperamos que este exemplo ajude você a integrar o ONLYOFFICE em seu aplicativo para

oferecer a edição online protegida por JWT para seus usuários. Mais exemplos de integração podem ser encontrados no GitHub. Você também pode encontrar mais informações sobre a implementação do JWT na documentação da API do ONLYOFFICE.