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.