在之前的文章《编写 Nginx 模块进行 RSA 加解密》中,我提到了如何编写 Nginx 模块,并借助 Nginx 实现相对高性能的加解密。正巧 Nginx 新版本发布,初步具备了原生“RSA加解密”能力。
那么,就来换一种更轻量的方式进行实现之前提到的功能吧。
写在前面
随着 Nginx 版本来到了 1.21.4 ,NJS 也升级到了 0.7 版本。这个版本可以说是具有突破意义的版本,因为这个版本的 NJS 添加了符合 W3C 的标准的 WebCrypto API。
这意味着,以往需要单独起一套服务来说接口加密鉴权的时代或许可以过去了。
官方实现这个功能主要是通过添加 njs_webcrypto.c
加解密模块,引入了部分 OpenSSL 的能力。如果你的需求包含了针对指定的 RSA 密钥(带密码)的加解密,那么目前 NJS 还做不到。不过,你可以修改上面的代码,添加我在《编写 Nginx 模块进行 RSA 加解密》一文中,提到的“计算部分”的代码实现:将 PEM_read_bio_RSAPrivateKey
携带密码的部分添加进去,并对 NJS 做一些函数绑定,最后记得清理干净 RSA 相关引用就好了。
好在在多数情况下,考虑到调用性能,针对业务接口进行加解密,不太倾向使用添加密码的密钥。
接下来,我将介绍如何使用 Nginx NJS 的这个新能力,一步步的实现一个能够根据业务接口内容,进行 RSA 自动加解密的接口服务。
使用浏览器生成 RSA 证书
你没有看错小标题,这次我们要使用浏览器而不是“传统命令行中OpenSSL”来生成我们的证书。
这里主要会用到两个 API:
- SubtleCrypto.generateKey()
- SubtleCrypto.exportKey()
文档枯燥,这里直接划出重点。在生成算法中,本文采用 WEB Crypto API 唯一支持的非对称加密算法 RSA-OAEP
,在导出生成证书时,需要根据密钥类型,针对性的选择对应的导出格式。
为了方便我的读者玩耍,我写了一段简单的 JavaScript 脚本,将内容复制粘贴到你的浏览器控制台里(推荐 Chrome ),然后执行即可。不出意外,你的浏览器将会自动下载到两个名为 “rsa.pub
”和“rsa.key
”文件,我们稍后会使用。
1 | js复制代码(async () => { |
使用 NJS 进行 RSA 加解密
虽然 Nginx 和 NJS 官方文档中,还未提及新添加的 WEB Crypto API 如何使用,但是我们可以从代码仓库中最新的测试用例中看到接口的用法。
我们参考之前的文章《使用 Docker 和 Nginx NJS 实现 API 聚合服务(前篇)》中“使用 NJS 编写 Nginx 基础接口”的代码为基础,先写一个“糙一些”的版本出来,体验下使用 NJS 进行 Nginx 原生 RSA 加解密:
1 | js复制代码const fs = require('fs'); |
上面的代码定义了一个简单的接口“simple”,用于加载我们刚刚生成的 RSA Keys,然后对一段指定的内容(originText
)进行加密再解密。将上面的内容保存为 app.js
,我们继续编写一段简单的 Nginx 配置(nginx.conf
):
1 | bash复制代码load_module modules/ngx_http_js_module.so; |
为了使用简单,这里同样给出一份容器配置(docker-compose.yml
):
1 | yaml复制代码version: '3' |
使用 docker-compose up
启动容器,然后在浏览器中访问 localhost:8080
,可以看到下面的内容。
顺便看一下响应时间,在笔记本的容器里大概十来ms,如果放到生产环境,加上一些优化,控制在个位数里问题不大。
好了,能力验证到此就结束了。我们来稍加改造和优化,实现网关产品中的全自动的 RSA 加解密功能。
构建具备 RSA 加解密能力的网关
下面具体实战一下,如何使用 Nginx 的 NJS 针对请求进行加解密。先来编写 Nginx 配置部分。
调整 Nginx 配置使用的 NJS 导出函数
考虑到调试方便,我们将“入口点”(接口)拆分为三个,你可以根据实际使用场景进行调整,比如在入口处添加 IP 访问限制、额外的身份验证功能,或者取消“统一的入口”,直接使用两个主要的加解密接口为程序“入口点”:
1 | bash复制代码server { |
完成了 Nginx 配置的编写后,就可以开始正餐了:编写 NJS 程序。
调整 NJS 程序:调整导出函数
Nginx 配置修改之后,同样的, NJS 中的导出函数也需要进行调整:
1 | bash复制代码export default { encrypt, decrypt, entrypoint }; |
修改完毕导出函数后,我们依次来实现三个接口函数的功能。
实现 NJS 程序:默认入口函数
因为目前 NJS 的开发调试还处于非常不方便的状态,所以我们先来编写入口函数,以方便调试过程(app.js
):
1 | bash复制代码function debug(req) { |
上面60来行代码中,我们实现了哪些功能呢?
- 一个简单的 Web 表单界面,用于接收我们调试开发过程中的“加解密动作”、“需要加解密的数据”。
- 根据我们选择的动作,自动进行“加解密”操作,并返回具体加解密接口的处理结果。
- 简单 Mock 了加解密接口,目前实际调用一个名为
debug
的函数打印我们的提交内容。
使用浏览器访问界面,能够看到这个简单的提交界面:
在调试表单里的文本框中随便写一点内容,进行提交,可以看到函数运行符合预期,提交内容被正确的打印了出来:
接着,我们来实现 NJS 的 RSA 加密函数。
实现 NJS 程序:RSA 加密函数
参考前文,稍作调整,不难实现这个加密函数,大概五行左右就够了。
1 | js复制代码async function encrypt(req) { |
再次运行 Nginx ,提交内容,可以看到数据已经被顺利的进行了 RSA 加密。
因为 RSA 默认加密后的内容不具备可读性,所以一般情况下,如果明文传输,我们会套一层 Base64 来展示。所以,我们需要对这个函数以及上一步中的函数进行一些调整,先拿入口函数“开刀”。
1 | js复制代码function entrypoint(r) { |
我们在调试入口添加了一个是否开启 Base64 编码的选项,并在开启 Base64 编码的情况下,调用加解密接口的时候,额外添加了一个 ?base64=1
的请求参数。
加密函数的改造也很简单,差不多十行就行了:
1 | js复制代码async function encrypt(req) { |
重启 Nginx 服务,选择使用 Base64 编码,可以看到输出结果已经符合预期了。
将内容复制保存,稍后使用。我们来接着实现 RSA 解密功能。
实现 NJS 程序:RSA 解密函数
有了 RSA 加密函数,写出解密函数就更简单了,这里就不和加密函数一样,拆解步骤了,直接照顾到“是否启用 Base64”这个选项类型就好。
1 | js复制代码async function decrypt(req) { |
使用上一步里的 Base64 后的 RSA 加密结果进行提交,可以看到我们前文中加密的内容就能被正确解密了。
有了上面的基础,接下来我们来折腾自动化加解密。
构建具备自动加解密能力的网关
为了模拟真实业务场景,我们得分别调整 Nginx 配置、容器配置。
调整 Nginx 配置:模拟业务接口
还是先进行 Nginx 配置的调整。
先模拟两个新的服务,并设定它们输出的内容,分别为原始数据和已被 RSA 加密过的数据。为了保持简单,我们还是使用 NJS 来模拟服务端接口响应内容:
1 | bash复制代码server { |
为了在模拟服务中使用 NJS,记得在 Nginx 全局配置中添加额外的 NJS 脚本引用声明:
1 | bash复制代码js_import mock from script/mock.js; |
为了方便本地调试,我们还可以调整容器编排配置,将上面两个服务的接口公开出来:
1 | yaml复制代码version: '3' |
实现 NJS 程序:编写业务模拟接口
这里参考上文,可以迅速写出两个业务接口,它们分别会输出后续需要加密原始数据,以及RSA加密后的数据。为了模拟真实场景,这里使用随机函数,随机的针对三个不同的内容进行具体计算。
1 | js复制代码function randomPick() { |
一切就绪之后,我们访问不同的端口,可以看到“业务接口”已经就绪啦。这里通过对已加密的数据添加 Encode-State
请求头,来做数据类型区别。如果你不希望添加额外字段,也可以在 Content-Type
中进行响应数据类型标识。
调整网关 Nginx 配置:聚合业务接口
业务实际使用方法有两种,一种是业务接口调用我们前文中的网关加解密功能,进行数据加解密,然后进行响应。而另外一种,则是网关聚合业务接口,根据数据响应类型调整对应的输出结果。
本文选择后一种方案,搭配 Traefik 可以实现快速的水平扩容,以提高服务响应能力。
因为 NJS 的子请求有请求来源限制,为了能够和业务数据进行交互,需要在网关的 Nginx 配置中添加两个接口,代理远端的需要加密或解密的业务数据。
1 | bash复制代码location /remote/need-encrypt { |
配置完毕,你就可以通过 http://localhost:8080/remote/need-encrypt
和 http://localhost:8080/remote/need-encrypt
访问上一小节中的内容了。
同时,为了我们能够访问自动加解密的接口,还需要再添加一个接口,用于调用 NJS 函数进行数据的自动加解密。(实际业务使用,追求极致性能,可以考虑拆分成两个)
1 | bash复制代码location /auto{ |
实现 NJS 程序:自动加解密业务数据
我们先来实现一个能够根据我们指定的数据源(加密过的数据、未解密的数据),进行数据的自动处理。
1 | js复制代码async function auto(req) { |
重启 Nginx ,分别访问代理远端数据接口 /remote/need-encrypt
和自动加密的网关接口,可以看到程序已经能够符合预期的运行了。
为了让程序更智能一些,达到数据加解密的完全自动化,可以再进行一个简单调整,让程序不是根据我们指定的参数去访问原始数据,而是随机访问原始数据。(为了能够直观验证行为,这里我们将输出内容也进行调整)
1 | js复制代码async function auto(req) { |
再次重启 Nginx ,多刷新几次,就能看到根据内容自动进行 RSA 加解密的结果啦。
其他:接口安全考虑
实际使用过程中,除了推荐在业务前添加额外的鉴权验证、频率限制外,同样建议根据实际情况使用 internal
限制 Nginx 接口的“作用域”,让数据源和基础计算接口仅允许被 NJS 程序内部访问。
1 | js复制代码location /remote/need-encrypt { |
其他:如果你追求更高效的计算
上面为了演示,我们将计算结果都进行了 Base64 编码,考虑实际生产环境中超高压力,我们一般对函数计算复杂度锱铢必较,所以可以考虑将证书硬编码到代码中,以及尽可能的去掉不必要的 Base64(只在调试模式中打开)。
最后
网络上关于 NJS 的参考资料目前还是比较少的,希望本文会成为连接你和 NJS 的纽带。
上述内容,我存放在了 GitHub 上,感兴趣的同学可以自取。
–EOF
我们有一个小小的折腾群,里面聚集了几百位喜欢折腾的小伙伴。
在不发广告的情况下,我们在里面会一起聊聊软硬件、HomeLab、编程上的一些问题,也会在群里不定期的分享一些技术沙龙的资料。
喜欢折腾的小伙伴欢迎扫码添加好友。(添加好友,请备注实名,注明来源和目的,否则不会通过审核)
如果你觉得内容还算实用,欢迎点赞分享给你的朋友,在此谢过。
本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 署名 4.0 国际 (CC BY 4.0)
本文作者: 苏洋
创建时间: 2021年11月14日
统计字数: 16413字
阅读时间: 33分钟阅读
本文链接: soulteary.com/2021/11/14/…
本文转载自: 掘金