锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

如何使用JSON Web令牌(JWT)保护您的文档

时间:2024-01-05 11:07:01 2静态交流断电延时继电器zsj

如何使用JSON Web令牌(JWT)保护您的文档

JWT

在本文中,我们解释了如何使用它JSON网络令牌JWT保护在线文档免受未经授权的访问,从而更安全地将在线文档编辑器开发集成到您自己的网络应用中。

这里将集成开源办公套件ONLYOFFICE Docs:

  • 文档、表格、幻灯片、表单模板编辑功能
  • 与微软Office文件格式(docx、xlsx、pptx)的高度集成
  • 实时协作

文档、表格、幻灯片和表单模板编辑功能如下图所示Office高度兼容,其中第二张表格截图保留了浏览器窗口的标题栏,其它截图都为网页全屏F截取获11模式下获得。



第一步:创建项目框架

假设已经安装好了Node.js,这里可以参考如何安装。

为工程创建文件夹,打开以下命令:

npm init 

提示我们设置包名、版本号、license等待信息,也可以直接跳过,可以创建这些信息package.json。

然后安装express:

npm install express --save 

这里需要npm的–save参数,在package.json文件中指定的项目依赖于文件中指定的项目express包。

创建以下文件:

  • index.jx 启动并配置express服务器
  • app/app.js 查询处理逻辑
  • app/config.json 可变参数,如端口号、编辑器地址等。json文件,但在真实项目中最好使用更可靠的方法)

index.js必须包括以下代码:

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}`); }); 

config.js文件必须包含文档编辑器的端口号:

{ 
        "port": 8080} 

创建公共文件夹,添加文件index.html,向package.json文件添加如下:

"scripts": { 
         "start": "node index.js" } 

以下命令开始运行app:

npm start 

打开浏览器测试htp://localhost:8080

第二步:打开文档

集成ONLYOFFICE的编辑器,需要安装ONLYOFFICE Document Server文档服务器。最简单的方式是使用Docker安装,仅需一行命令即可:

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

文档服务器必须能够向这个服务器发送http请求,并且能接收处理服务器返回的请求。

config.json添加编辑器(文档服务器)和示例app的地址,类似如下:

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

在这个阶段,应该向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
}

添加pug包:

npm install pug --save

既然已经安装pug模板引擎,就可以删除index.html了。创建一个查阅文件夹,在index.js中添加下面代码连接引擎:

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

然后就可以创建views/index.pug,添加创建文档、打开文档的按钮:

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

逻辑将在app/app.js中讲解:创建一个文件(或者检查是否已经存在),然后格式化编辑器的配置,可以阅读这里查看细节,然后返回页面模板:

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
};

在这里,加载编辑器脚本http://docserver/web-apps/apps/api/documents/api.js然后添加编辑器的实例new DocsAPI.DocEditor("iframeEditor", !{cfg})

现在运行app测试一下。

第三步:编辑文档

编辑文档,更准确的说,是保存您的修改。这需要处理从文档服务器发来的修改保存请求,在配置文件中指定如何响应这个请求,关于文档服务器的请求可以参考这里。

文档服务器发送带有JSON内容的POST请求,这就是为什么我们需要连接到中间件来从JSON解析到index.js

app.use(express.json());

为了第一时间接收它,应告诉文档服务器如何处理,向编辑器的配置文件中添加callbackUrl: cfg.example_root + "callback?filename=" + fileName

然后创建一个回调函数,从文档服务器获取信息,检查请求状态:

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();
}

在这个例子里,只关注文档保存的请求处理,一旦接收到保存文件请求,我们将从POST数据中获取指向我们文档的链接并将其保存到我们的文件系统中:

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();
            });
       }
    });
}

现在我们就有了一个具备文档编辑功能的网页应用了,接下来使用JWT来保护它免受未授权的访问。

第四步:实施JWT

ONLYOFFICE使用JSON网络令牌保护在编辑器、内部服务以及存储空间之间的数据交换。它请求一个加密的签名,然后托管在令牌中。 此令牌校验对数据执行特定操作的权限。

如果打算使用JWT最好使用准备好的包,但是在这里为了理解工作原理将完全手动实现。

入门理论基础

JWT包含三部分:

  • 头:包含元信息,例如,一个加密算法
  • 负载:数据内容
  • hash哈希:基于上面两部分和密码的哈希值

所有这三部分是JSON对象,然而JSON令牌本身是由点符号(.)所连接的所有部分的base64URL编码。

工作原理:

  1. 服务器1依据一个密钥和一个header.payload的字符串计算一个哈希值。
  2. 令牌header.payload.hash生成
  3. 服务器2接收到这个令牌,依据它的前两部分生成哈希值。
  4. 服务器2比较生成的令牌和接收到的令牌,如果匹配,那么就说明数据没有被修改

现在为这个集成实例实现JWT令牌

编辑器允许在请求包头和正文中传输JWT令牌,使用请求包正文部分比较好,因为数据包头空间有限,但是这里将考虑所有情况。

如果选择包头传输令牌,需要使用负载key密钥来将数据加入对象中。

如果选择包正文传输令牌,负载类似如下:

{ 
        
"key": "value"
}

使用包头传输令牌:

{ 
        
"payload": { 
        
"key": "value"
   }
}

config.json添加key密钥:

"jwt_secret": "supersecretkey"

开启JWT启动编辑器还需要设定环境变量:

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

如果使用包正文传输令牌,还需添加一个变量-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

app/jwtManager.js包含JWT的所有逻辑,只需要在打开编辑器的时候向配置添加令牌:

if (jwt.isEnabled()) { 
        
editorConfig.token=jwt.create(editorConfig);
}

令牌本身有上面理论解释的算法来计算生成,代码如下:

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"));
}

这样就打开了一个文档,但也要检查一下从文档服务器接收到的令牌。

要检查包正文和包头,函数很简单,如果有问题它就会抛出错误,否则,确认了令牌后将合并包正文和令牌负载:

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);
    }
}

校验函数也很简单:

function verify(token) { 
        
    if (!isEnabled()) return null;
    if (!token) return null;
 
    var parts = token.split(".");
    if (parts元器件数据手册IC替代型号,打造电子元器件IC百科大全!
          

相关文章