百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

编写 Nginx 模块进行 RSA 加解密_nginx rmtp

off999 2025-02-21 14:58 36 浏览 0 评论

写在前面

实现一个具备 RSA 加解密的功能并不难,使用任何一门“具备流行度”的语言,调用相关函数库/模块进行操作即可,一般只需要两步:

  • 创建或加载 RSA KEY(s)。
  • 设置使用算法等参数,进行计算,得到结果。

但是,即使我们需要提供一个最简单的无需鉴权的开放接口,还会有非常多的额外工作要做,诸如:如何提供基础 Web 服务,如何进行云端架构设计,如何高效利用服务器资源,如何保证服务质量,如何进行监控…然而,以上多数的事情都偏离了我们最初的初衷,需要额外编写或进行大量的设置。有没有更省事的方案呢?

众所周知,Nginx 是一个高性能的 Web 服务器,易于部署,同时又具备很强的可扩展性,如果在上面添加我们所需要的功能,这个开放接口的需求不就能以最小代价实现了嘛?

社区里其他方案和不足之处

在实现之前,我在开源社区中进行了简单的调研,发现这个需求目前已有两个实现:

  • https://github.com/spacewander/lua-resty-rsa
  • https://github.com/LittleLiByte/lua-rsa

这两个实现,都是基于 Nginx 衍生版 OpenResty 的模块,两者功能上的重要区别在于前者只支持基于公钥加密私钥解密,而对于私钥加密公钥解密的方式却不支持,后者则进行了这个功能的补全;在实现上的差异主要在于前者重度使用了 LUA FFI 这个允许以 LUA 代码调用外部 C 函数库的库,将 OpenSSL 中关于 RSA 加解密的函数导入了 LUA 模块,而后者虽然也使用了相同的方案,不同的地方在于,使用了自行构建的 RSA 库来进行 OpenSSL 函数库的调用,在执行效率上有了很大的进步(在压测过程中,CPU 一度达到 20%+)。

不知是作者和我一样对于 C 语言并不“熟练”,还是倾向于使用 LUA,代码直接使用了2014年3月国外一位技术博客作者 Ravishanker Kusuma 的示例代码:RSA Encryption & Decryption Example with OpenSSL in C。

上面这段示例代码,或许也启发了前文中第一个库的方案,在 2014 年 123 月的时候,初代作者 doujiang24 的提交中对于加解密的方式和上面文章如出一辙:
https://github.com/spacewander/lua-resty-rsa/commit/9262c57e89fd3c554e6625cddead20781d34b43b,相比较原作者的示例代码,这个提交中可以看到作者添加了针对“包含密码保护的私钥”的加载处理。

不过,既然都考虑编译 C 语言函数库了,为什么不考虑再更进一步呢?尤其是针对这类持久不易变化的功能。直接编译成 Nginx 模块,显然可以获得更多的性能福利。

编写 Nginx 模块:完成基础计算部分

为了尽可能让文章简单,我将上面的代码进行了一定的精简和调整,只实现私钥加密的功能,并对外部使用的变量做了清理操作,减少了原始代码中内存溢出的问题,其他函数实现类似,在此就不多赘述。

考虑到 RSA 加密后的内容可读性不高,于是额外引入了一个简单的 Base64 编码实现。

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include 
#include 

char privateKey[] = "-----BEGIN ENCRYPTED PRIVATE KEY-----\n"
                    "MIIJrTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQICTqoISmC8M0CAggA\n"
                    ... ...
                    "onL8DKhku9s/5NB+eEVC3v4JubSfph0GEiVemMIQxMI2\n"
                    "-----END ENCRYPTED PRIVATE KEY-----\n";

char *passphrase = "soulteary.com";

int encrypt(unsigned char *data, int data_len, unsigned char *encrypted)
{
    BIO *keybio;
    keybio = BIO_new_mem_buf(privateKey, -1);

    RSA *rsa = PEM_read_bio_RSAPrivateKey(keybio, &rsa, NULL, passphrase);
    int ret_len = RSA_private_encrypt(data_len, data, encrypted, rsa, RSA_PKCS1_PADDING);

    BIO_free(keybio);
    RSA_free(rsa);

    return ret_len;
}

char *base64(const unsigned char *input, int length)
{
    BIO *bmem, *b64;
    BUF_MEM *bptr;

    b64 = BIO_new(BIO_f_base64());
    bmem = BIO_new(BIO_s_mem());
    b64 = BIO_push(b64, bmem);
    BIO_write(b64, input, length);
    BIO_flush(b64);
    BIO_get_mem_ptr(b64, &bptr);

    char *buff = (char *)malloc(bptr->length);
    memcpy(buff, bptr->data, bptr->length - 1);
    buff[bptr->length - 1] = 0;

    BIO_free_all(b64);

    return buff;
}

int main()
{

    char raw[2048 / 8] = "{\"key\": \"val\"}";
    unsigned char encrypted[4098] = {};

    int encrypted_length = encrypt(raw, strlen(raw), encrypted);
    if (encrypted_length == -1)
    {
        printf("Encrypt failed");
        exit(0);
    }

    char *result = base64(encrypted, encrypted_length);

    printf("%s\n", result);

    exit(0);
}

为了更方便的测试功能,将上面的内容保存为 encrypt.c,然后编写一个 Dockerfile 用于基础计算部分的编译测试(这里需要注意编译顺序,避免编译不通过):

FROM alpine:3.13 AS Builder

RUN cat /etc/apk/repositories | sed -e "s/dl-cdn.alpinelinux.org/mirrors.aliyun.com/" | tee /etc/apk/repositories
RUN apk add openssl-dev libressl-dev gcc g++
COPY ./encrypt.c /
RUN cd / && \
	gcc encrypt.c -lssl -lcrypto -o encrypt && \
	cp encrypt /bin/

使用 docker build -t test . 构建一个临时的容器,然后使用 docker run --rm -it test encrypt 测试程序,会得到类似下面的结果。

rFKPHoJPR4iExWgx6EFzLVA4uISNyxYDtmOlGT+e6SBy9SPDve4o5YNzSbX1grnj
bCFwvp80SwJzs1yaFzChDxo7HTdV3hK3syba+8zHw05FBeuw4/q8zn4e+KAv5QjE
KzrQvMlzE1XsPrbI+IjJpqGIrpy57VBVr8CpmT/RajqZ42fy/cgn429i3NJhlckW
vVPbY7x3vcXC/5FcRwR9hqPJ2qVXulVH/SxQ422bmLigFHwnWjT0qnDVTvgQFeQd
1edmJzPgbhycmGPvCdjRvN80eEX8lp3Oz92uACXfeReab26R0vhgGysPv3w97vdN
TPmt9l1eyeWSnYR/gWU9HSM7FbAJyvKLp5h07X4AYyf2uDl7DxWIJp+ZG/IxzCzt
2IzKN2siq2bqvJEqR7+wDS2ttgCzD2ogmMMiQFgAa1yDruRasSJbV408n7STmE6h
Z6klL8C+zXwgOLYnDi7bNVUhz6BkqHrt+utKql6zr0lKdOywwqElOQJeostBknH5
OCcoazuu+ZOOeAT3DoRLRVqHp/v75aIW9nYJmNjCqgonYrw9flKezh04nlSCjDaF
DYJKVUSsL0mAhGMxfcVWrEzWOTAIOvg6U3vshSpDKk3KCaRhamWxCMVItfBNeeHL
T8ZYF0tHz/cLkBm1wSNOuTxaGQRD/ZH0lSQGP8Aq4x4=

在验证完毕基础功能可行后,我们来看看怎么将它编写为一个 Nginx 可以调用的模块。

编写 Nginx 模块:按照 Nginx 模块要求进行改写

这里先不考虑让 Nginx 从请求中获取数据动态改变计算结果,仅做简单的代码调整,让 Nginx 能够调用我们之前实现的函数。

#include 
#include 
#include 

#include 
#include 
#include 
#include 
#include 
#include 
#include 

char privateKey[] = "-----BEGIN ENCRYPTED PRIVATE KEY-----\n"
                    "MIIJrTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQICTqoISmC8M0CAggA\n"
                    ... ...
                    "onL8DKhku9s/5NB+eEVC3v4JubSfph0GEiVemMIQxMI2\n"
                    "-----END ENCRYPTED PRIVATE KEY-----\n";

char *passphrase = "soulteary.com";
int rsa_encrypt(unsigned char *data, int data_len, unsigned char *encrypted)
{
    BIO *keybio;
    keybio = BIO_new_mem_buf(privateKey, -1);
    OpenSSL_add_all_algorithms();
    RSA *rsa = PEM_read_bio_RSAPrivateKey(keybio, &rsa, NULL, passphrase);

    int encrypted_length = RSA_private_encrypt(data_len, data, encrypted, rsa, RSA_PKCS1_PADDING);

    BIO_free(keybio);
    RSA_free(rsa);

    return encrypted_length;
}

char *base64(const unsigned char *input, int length)
{
    BIO *bmem, *b64;
    BUF_MEM *bptr;

    b64 = BIO_new(BIO_f_base64());
    bmem = BIO_new(BIO_s_mem());
    b64 = BIO_push(b64, bmem);
    BIO_write(b64, input, length);
    BIO_flush(b64);
    BIO_get_mem_ptr(b64, &bptr);

    char *buff = (char *)malloc(bptr->length);
    memcpy(buff, bptr->data, bptr->length - 1);
    buff[bptr->length - 1] = 0;

    BIO_free_all(b64);

    return buff;
}

static char *ngx_http_encrypt(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
static ngx_int_t ngx_http_encrypt_handler(ngx_http_request_t *r);

static ngx_command_t ngx_http_encrypt_commands[] = {
    {ngx_string("encrypt"),
     NGX_HTTP_LOC_CONF | NGX_CONF_NOARGS,
     ngx_http_encrypt,
     0,
     0,
     NULL},
    ngx_null_command};

static ngx_str_t will_encrypt_string;

static ngx_http_module_t ngx_http_encrypt_module_ctx = {
    NULL, /* preconfiguration */
    NULL, /* postconfiguration */

    NULL, /* create main configuration */
    NULL, /* init main configuration */

    NULL, /* create server configuration */
    NULL, /* merge server configuration */

    NULL, /* create location configuration */
    NULL  /* merge location configuration */
};

ngx_module_t ngx_http_encrypt_module = {
    NGX_MODULE_V1,
    &ngx_http_encrypt_module_ctx, /* module context */
    ngx_http_encrypt_commands,    /* module directives */
    NGX_HTTP_MODULE,              /* module type */
    NULL,                         /* init master */
    NULL,                         /* init module */
    NULL,                         /* init process */
    NULL,                         /* init thread */
    NULL,                         /* exit thread */
    NULL,                         /* exit process */
    NULL,                         /* exit master */
    NGX_MODULE_V1_PADDING};

static ngx_int_t ngx_http_encrypt_handler(ngx_http_request_t *r)
{
    ngx_int_t rc;
    ngx_buf_t *b;
    ngx_chain_t out;

    /* we response to 'GET' and 'HEAD' requests only */
    if (!(r->method & (NGX_HTTP_GET | NGX_HTTP_HEAD)))
    {
        return NGX_HTTP_NOT_ALLOWED;
    }

    /* discard request body, since we don't need it here */
    rc = ngx_http_discard_request_body(r);

    if (rc != NGX_OK)
    {
        return rc;
    }

    /* set the 'Content-type' header */
    r->headers_out.content_type_len = sizeof("text/html") - 1;
    r->headers_out.content_type.len = sizeof("text/html") - 1;
    r->headers_out.content_type.data = (u_char *)"text/html";

    /* send the header only, if the request type is http 'HEAD' */
    if (r->method == NGX_HTTP_HEAD)
    {
        r->headers_out.status = NGX_HTTP_OK;
        r->headers_out.content_length_n = will_encrypt_string.len;

        return ngx_http_send_header(r);
    }

    /* allocate a buffer for your response body */
    b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
    if (b == NULL)
    {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    char *preset_words = "hi";
    will_encrypt_string.data = (u_char *)preset_words;
    will_encrypt_string.len = strlen(preset_words);

    unsigned char *test_data = (unsigned char *)will_encrypt_string.data;
    int data_len = will_encrypt_string.len;

    unsigned char rsa_encrypted[4096] = {};
    int encrypted_length = rsa_encrypt(test_data, data_len, rsa_encrypted);

    char *base64_data = base64(rsa_encrypted, encrypted_length);
    will_encrypt_string.data = (u_char *)base64_data;
    will_encrypt_string.len = ngx_strlen(will_encrypt_string.data);

    /* attach this buffer to the buffer chain */
    out.buf = b;
    out.next = NULL;

    /* adjust the pointers of the buffer */
    b->pos = will_encrypt_string.data;
    b->last = will_encrypt_string.data + will_encrypt_string.len;
    b->memory = 1;   /* this buffer is in memory */
    b->last_buf = 1; /* this is the last buffer in the buffer chain */

    /* set the status line */
    r->headers_out.status = NGX_HTTP_OK;
    r->headers_out.content_length_n = will_encrypt_string.len;

    /* send the headers of your response */
    rc = ngx_http_send_header(r);

    if (rc == NGX_ERROR || rc > NGX_OK || r->header_only)
    {
        return rc;
    }

    /* send the buffer chain of your response */
    return ngx_http_output_filter(r, &out);
}

static char *ngx_http_encrypt(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_core_loc_conf_t *clcf;

    clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
    clcf->handler = ngx_http_encrypt_handler;

    return NGX_CONF_OK;
}

将上面的内容保存为 ngx_http_encrypt_module.c,然后编写模块所需要的声明文件:

ngx_addon_name=ngx_http_encrypt_module

if test -n "$ngx_module_link"; then
    ngx_module_type=HTTP
    ngx_module_name=$ngx_addon_name
    ngx_module_incs=
    ngx_module_deps=
    ngx_module_srcs="$ngx_addon_dir/ngx_http_encrypt_module.c"
    ngx_module_libs=
   . auto/module
else
    HTTP_MODULES="$HTTP_MODULES ngx_http_encrypt_module"
    NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_encrypt_module.c"
fi

在编译文件之前,我们先进行 Nginx 配置文件的编写:

load_module modules/ngx_http_encrypt_module.so;

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    keepalive_timeout  65;

    gzip  on;

    server {
        listen       80;
        server_name  localhost;

        location / {
            encrypt;
        }
    }
}

我这里使用 Nginx 默认配置进行简单修改,在文件头部声明加载接下来编译生成的动态模块,在 location 配置中添加我自定义的 encrypt 指令,然后将上面的内容保存为 nginx.conf 等待后面使用。

接着,借助我之前文章《如何在容器时代高效使用 Nginx 三方模块》中提到过的 工具镜像,就能进行快速进行 Nginx 模块的编译开发了,还是先来编写一个用于编译插件的 Dockerfile:

FROM soulteary/prebuilt-nginx-modules:base-1.21.1-alpine AS Builder
COPY src/config /usr/src/encrypt/
COPY src/ngx_http_encrypt_module.c /usr/src/encrypt/
WORKDIR /usr/src/nginx
RUN CONFARGS=$(nginx -V 2>&1 | sed -n -e 's/^.*arguments: //p') \
    CONFARGS=${CONFARGS/-Os -fomit-frame-pointer -g/-Os} && \
    echo $CONFARGS && \
    ./configure --with-compat $CONFARGS --add-dynamic-module=../encrypt && \
    make modules

FROM nginx:1.21.1-alpine
COPY nginx.conf /etc/nginx/nginx.conf
COPY --from=Builder /usr/src/nginx/objs/ngx_http_encrypt_module.so /etc/nginx/modules/

还是执行 docker build -t test . 构建一个基础镜像,然后执行 docker run --rm -it -p 8080:80 test 来进行功能测试。浏览器或终端访问本地的 8080 端口,不出意外,能够看的我们预期中 RSA 加密后的内容被正确的 Base64 编码后展示了出来。

使用 wrk 之类的软件进行压力测试,会发现相比较使用 Lua 方案,使用纯 Nginx 模块的方案,CPU 负载从 20% 左右降低到了个位数,甚至持续稳定在 1% 以内,结果还是比较惊艳的。

编写 Nginx 模块:改进模块,支持参数

接下来,我们来进行一个最常规的功能支持,让模块能够接受请求参数,并根据参数的变化,动态计算结果。

...
/* allocate a buffer for your response body */
b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
if (b == NULL)
{
    return NGX_HTTP_INTERNAL_SERVER_ERROR;
}

ngx_str_t var_user = ngx_string("arg_user");
ngx_uint_t key_user = ngx_hash_key(var_user.data, var_user.len);
ngx_http_variable_value_t *val_user = ngx_http_get_variable(r, &var_user, key_user);

if (val_user != NULL && val_user->not_found != 1)
{

    char *tpl;
    asprintf(&tpl, "{\"name\":\"%s\"}", (char *)val_user->data);

    will_encrypt_string.data = (u_char *)tpl;
    will_encrypt_string.len = strlen(tpl);
    free(tpl);
}
else
{
    char *preset_words = "hi";
    will_encrypt_string.data = (u_char *)preset_words;
    will_encrypt_string.len = strlen(preset_words);
}

unsigned char *test_data = (unsigned char *)will_encrypt_string.data;
int data_len = will_encrypt_string.len;

...

上面的代码展示了如何读取 GET 请求中的 user 参数,并将参数安全的拼合到字符串模版中,进行计算。相信即使你不熟悉 C 语言,参考上面的代码也可以快速进行调整,将计算逻辑调整到符合你预期的方式。

如果你希望模块能够非编译环境快速复用,可以参考其他的 Nginx 插件源代码,“实现 Nginx 读取静态文件”的功能,将编译至模块中的证书剥离出来。不过,如果你有 CI 环境,能够方便的持续获取最新的构建结果,将证书编译至模块中,可以获得更高的绝对性能。

其他

关于 Nginx 模块的开发,我个人建议阅读 Nginx 官方开发文档 和 Nginx 源代码,相比较国内外陈旧的文档,可以节约不少时间。

如果你对于加密内容的长度有特殊要求,需要加密特别长的内容,我建议你阅读这篇文章,考虑使用 EVP 函数替换文中的函数执行加密过程:Simple Public Key Encryption with RSA and OpenSSL。

最后

下一篇文章,我将聊聊我将本文中的功能用到了哪里。

相关推荐

project2010激活码(project2010永久激活码)

首先我们在网上下载project2010,因为project2010是付费使用的,所以我们可以先使用试用版,试用版期限是30天。首先我们来创建一个项目,然后使用project2010来进行管理。我们可...

迈克菲无法卸载(迈克菲无法卸载怎么解决)

一,找到迈克菲的图标二,右键,点击更改设置——实时扫描三,点击关闭,即可。1,控制面板中-管理工具-服务-停止MCAFEEFRAMEWORKMANAGER服务2,开始中—运行—输入cmd—输入“C...

爱普生打印机驱动(爱普生打印机驱动怎么安装)

1.首先进入爱普生网站。找到驱动以及手册证书下载2.选择打印机的类型,勾选正确的打印机型号。3.根据自己的电脑操作系统选择对应的软件版本点击下载。4.下载了LQ-630KII的驱动,双击运行。5.运行...

windows7显示不是正版怎么办

不必担心,是不是正版影响并不大,不会影响你正常使用电脑,无非就是心里对于正版和盗版的道德观念会有点小执念罢了,但这些都不影响系统的正常使用。如果你非要安装正版,可以去买个正版光盘,安装系统后输入正版激...

vivo手机锁屏密码忘了怎么办

如下:1输入密码在vivo的锁屏页面,连续输入五次错误密码,弹出新页面。2/4选择忘记密码在新的页面中,找到页面中的忘记密码这个选项,点击它。3/4进行身份验证切换到身份验证的页面后,回答两个问题,再...

微软官网免费下载win7吗(微软官网免费下载win10是真的吗)
微软官网免费下载win7吗(微软官网免费下载win10是真的吗)

最简单的方法就是,下载完镜像文件后,直接把镜像文件解压,解压到非C盘,然后在解压文件里面找到setup.exe,点击运行即可。安装系统完成后,在C盘找到一个Windows.old(好几个GB,是旧系统打包在这里,垃圾文件了)删除即可。扩展资...

2025-12-17 00:51 off999

windows7的密钥在哪里(windows7密钥在哪里可以找到)

查看Windows7的密钥,你可以按照以下步骤进行操作:在键盘上按下Win+R组合键,打开运行窗口。在运行窗口中输入"regedit",然后点击确定,打开注册表编辑器。在注册表编...

360极速浏览器打不开网页(360极速浏览器经常打不开未响应)

先打开任务管理器结束360浏览器的所有进程(如果有).再尝试点击启动.如果还不行,重启电脑再试,如果还不行,执行杀毒检测和磁盘扫描,并卸载重装浏览器.强制360浏览器以极速模式打开页面方法如下:1、...

win7系统图标(win7系统图标网络开关灰色)

1/6首先点击桌面右下角Windows,再点击【控制面板】2/6来到控制面板页面,我们再点击【个性化】3/6来到个性化页面,我们再点击左侧【更改桌面图标】4/6弹出桌面图标设置对话框,我们根据实际需要...

系统的超级宗门等级(系统的超级宗门人物介绍)

《一念永恒》中宗门等级划分分为:顶级宗门、一流宗门、二流宗门、三流宗门和小宗门。顶级宗门位于武道源头,是无数武极强者梦寐以求的存在。一流宗门规模庞大,经济实力雄厚,拥有大量高手强者,地位颇为显赫。二流...

为啥系统重装后有两个系统(为啥系统重装后有两个系统 原来的系统还在)

电脑重装系统后有两个系统,需要重新安装,并且再安装系统时需要把原来的系统所在盘即C盘进行格式化,否则安装完成就还是两个系统,非常占系统内存。1、可能在安装时删除了原来的引导分区。2、可能安装时直接安装...

win10win7双系统引导设置(win10 win7双系统引导)

 步骤如下:  1、首先我是开机时按F8,进入安全模式界面。但是进去的时候等待了几分钟都无反应,上面一直显示请稍等。  2、没办法只有按下复位键重启电脑,因电脑之前装有一键GHOST备份,于是果断还原...

ie缓存清理在哪里(ie缓存如何清除)

?  1、首先打开IE浏览器,选择IE浏览器的工具这一选项;  2、下一步选择工具中的Internet的选项;  3、下一步就是在Internet选项中的常规的选项中;  4、选择常规--浏览历史记录...

华为正版鸿蒙40电脑操作系统下载中文版

安装华为鸿蒙40系统正式版需要先下载官方固件包,然后将固件包放到手机内部存储或外部存储卡中。打开手机设置,选择系统更新,点击“手动更新”,选择已下载的固件包进行安装。安装前请备份重要数据并确保手机电量...

笔记本电脑哪个牌子好用又实惠

1.神舟优雅X4优点:1.35kg很轻巧,14英寸够便携固态硬盘,速度快,有背光键盘。缺点:配置较低,只能轻度办公,售后一般。2.攀升MaxBookP1优点:零噪音,金属机身,固态硬盘,大触摸板,背...

取消回复欢迎 发表评论: