15. Nginx HTTP Filter 开发实战:Header 与 Body Filter全解析
1. 引言Nginx 凭借其高性能、模块化的架构已经成为现代 Web 基础设施的核心组件。在日常开发中我们经常需要在请求处理或响应返回前对数据进行加工例如添加安全响应头、压缩响应体、替换敏感信息、注入统计脚本等。这些需求都可以通过开发HTTP Filter 模块来实现。与“内容处理模块”handler不同filter 模块工作在 Nginx 内部处理链条上对所有经过的请求和响应透明地施加影响。本文将通过一个完整的实战案例带你深入理解 Nginx HTTP Filter 模块的编写方法重点剖析header filter和body filter的注册、上下文管理以及与业务场景的结合。2. Nginx HTTP 处理流程与 Filter 定位在开始编码之前先回顾一下 Nginx 的 HTTP 处理阶段。HTTP 模块通常分为三大类Handler 模块直接产生内容如ngx_http_static_module、ngx_http_proxy_module。Filter 模块对 Handler 产生的内容或上游返回的响应进行过滤加工。Upstream 相关模块负责与后端服务器交互。Filter 模块串联成一个链条分为header filter和body filter两个主要阶段。Header filter 在响应头生成后、发送给客户端之前执行Body filter 在响应体数据块传递时被多次调用直到数据发送完毕。下图展示了这一处理链条是否客户端请求Handler 生成内容/上游返回Header Filter 链写入响应头Body Filter 链发送响应体数据块还有数据?连接关闭我们的目标就是在C和E两个环节插入自定义逻辑。3. Filter 模块基础结构与核心钩子一个典型的 HTTP Filter 模块需要完成以下工作定义模块上下文ngx_http_module_t用于存放模块级别的配置。注册模块通过ngx_module_t结构体将模块暴露给 Nginx。实现配置指令如果模块有配置项需要定义ngx_command_t数组。实现 filter 函数分别是 header filter 和 body filter并设定它们的调用顺序优先级。我们先来看模块的最基本骨架完整代码见最后一节。这里只强调几个关键点filter 链的注册通过ngx_http_top_header_filter和ngx_http_top_body_filter全局变量将自定义函数插入链的头部。这两个变量是函数指针的链表新模块调用时需要保存原有指针并在自己函数末尾调用原函数以保证链的延续。上下文ctx的保存与传递body filter 可能被多次调用以处理分块数据因此我们需要在请求的ngx_http_request_t的ctx数组中分配一个位置存放自己的上下文结构体用于记录状态例如是否已经执行过某个操作。优先级模块定义中的NGX_MODULE_V1宏包含模块类型、模块索引等信息但真正的执行顺序取决于注册时插入链表的方式通常我们插入到头部。4. Header Filter 开发自定义安全响应头4.1 业务场景假设我们需要为所有通过 Nginx 返回的 HTTP 响应添加一些安全相关的标准头部X-Content-Type-Options: nosniffX-Frame-Options: DENYX-XSS-Protection: 1; modeblock这些头部的添加不应影响上游服务本身的响应头设置且最好在一个集中的地方实现避免遗漏。此时 header filter 是最佳选择。4.2 实现方案我们将模块命名为ngx_http_myfilter_module头文件与源文件结构如下config模块编译配置文件。ngx_http_myfilter_module.c主模块代码。模块上下文与指令配置我们的模块不需要指令全自动生效因此ngx_command_t直接置空。模块上下文ngx_http_module_t也只保留最基本的成员。Header Filter 函数实现函数原型为ngx_int_t (*)(ngx_http_request_t *r)返回NGX_OK成功或NGX_ERROR错误。需要注意如果请求的目标是子请求r ! r-main通常不对子请求单独施加 header filter除非需要特殊处理。修改响应头需要使用ngx_table_elt_t结构体。可以直接创建一个新的 header 并插入到r-headers_out.headers链表中并根据需要设置 hash。代码示意staticngx_int_tngx_http_myfilter_header_filter(ngx_http_request_t*r){ngx_table_elt_t*h;/* 忽略子请求 */if(r!r-main){returnngx_http_next_header_filter(r);}/* 添加 X-Content-Type-Options */hngx_list_push(r-headers_out.headers);if(hNULL){returnNGX_ERROR;}h-hash1;ngx_str_set(h-key,X-Content-Type-Options);ngx_str_set(h-value,nosniff);/* 添加 X-Frame-Options */hngx_list_push(r-headers_out.headers);if(hNULL){returnNGX_ERROR;}h-hash1;ngx_str_set(h-key,X-Frame-Options);ngx_str_set(h-value,DENY);/* 添加 X-XSS-Protection */hngx_list_push(r-headers_out.headers);if(hNULL){returnNGX_ERROR;}h-hash1;ngx_str_set(h-key,X-XSS-Protection);ngx_str_set(h-value,1; modeblock);returnngx_http_next_header_filter(r);}每次调用ngx_list_push从请求内存池分配新的 header 节点。设置hash1是为了让 Nginx 不过滤重复头部默认情况下相同 hash 值的头部会被去重这里我们置为 1 表示强制加入因为安全头部并不需要 hash 功能。最后必须调用ngx_http_next_header_filter(r)将控制权交给下一个 header filter。5. Body Filter 开发响应体内容替换5.1 业务场景某老旧系统返回的 HTML 内容中包含硬编码的 HTTP 链接我们希望统一升级为 HTTPS同时替换一些过期的内网域名。这要求在响应体流经 Nginx 时进行内容替换且不能改变响应头如Content-Length导致客户端解析错误。由于 Nginx 可能分 chunk 传输body filter 需要支持分块处理。5.2 上下文设计因为 body filter 可能被多次调用我们必须保存替换操作的中间状态。我们在请求的ctx中分配一个位置并定义上下文结构体typedefstruct{ngx_chain_t*last_buf;/* 暂存未处理完的缓冲区尾部 */ngx_flag_tdone;/* 是否已处理完整个响应体 */}ngx_http_myfilter_ctx_t;在模块的postconfiguration或init中通过ngx_http_next_header_filter等机制不是必须的更重要的是在模块初始化时如ngx_http_myfilter_init为 body filter 链注册我们的函数并为ctx分配索引。模块级别的ctx_index通过ngx_http_get_module_ctx/ngx_http_set_ctx宏操作。索引在模块初始化时由 Nginx 自动分配我们只需在postconfiguration阶段保存。而请求级别的ctx是ngx_http_request_t的一个数组索引为该模块的ctx_index。 所以我们在模块的ngx_http_module_t的ctx_index中记录索引值并在 body filter 中这样获取上下文staticngx_http_module_tngx_http_myfilter_module_ctx{NULL,/* preconfiguration */NULL,/* postconfiguration */NULL,/* create main conf */NULL,/* init main conf */NULL,/* create server conf */NULL,/* merge server conf */NULL,/* create location conf */NULL/* merge location conf */};staticngx_int_tngx_http_myfilter_init(ngx_conf_t*cf){ngx_http_next_header_filterngx_http_top_header_filter;ngx_http_top_header_filterngx_http_myfilter_header_filter;ngx_http_next_body_filterngx_http_top_body_filter;ngx_http_top_body_filterngx_http_myfilter_body_filter;returnNGX_OK;}但在实际代码中我们会在ngx_http_module_t里设置postconfiguration为ngx_http_myfilter_init。上下文索引的保存需要在ngx_http_module_t内其实ngx_http_module_t本身不再包含ctx_index那个字段在ngx_module_t中。但是ngx_http_get_module_ctx使用的module指针需要我们自己维护一个全局变量指向当前模块。更常见的模式是定义一个全局ngx_module_t变量然后在 body filter 中使用ngx_http_get_module_ctx(r, ngx_http_myfilter_module)其中第二个参数就是指向ngx_module_t的指针。因此我们模块的ngx_module_t定义如ngx_module_tngx_http_myfilter_module{NGX_MODULE_V1,ngx_http_myfilter_module_ctx,/* module context */NULL,/* 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};在 body filter 中获取/创建上下文ngx_http_myfilter_ctx_t*ctx;ctxngx_http_get_module_ctx(r,ngx_http_myfilter_module);if(ctxNULL){ctxngx_pcalloc(r-pool,sizeof(ngx_http_myfilter_ctx_t));if(ctxNULL){returnNGX_ERROR;}ngx_http_set_ctx(r,ctx,ngx_http_myfilter_module);}5.3 实现 Body Filter函数原型为ngx_int_t (*)(ngx_http_request_t *r, ngx_chain_t *in)。in是待处理的 buf 链表。处理逻辑获取模块上下文ctx。遍历in链表对每个 buf 进行内容查找替换http-httpsold.example.com-new.example.com。由于直接原地修改可能会由于替换后长度变化导致Content-Length不一致更安全的做法是重新分配缓冲区大小需要计算新的长度。将所有修改后的 buf 拼接成新的ngx_chain_t输出并调用ngx_http_next_body_filter(r, out)。当in为NULL时表示这是最后一个空缓冲区请求结束设置ctx-done 1并清理。为了简单演示我们这里假设替换后的长度不超过原始长度将http://替换为https://会增加长度但替换后可能超出原 buf 容量。严谨的处理需要动态计算新 buf 长度并分配新ngx_buf_t。本文将展示一个简化但可工作的版本预先分配一个足够大的临时缓冲区或者使用ngx_palloc创建新的 buf。同时我们还会处理链式 buf 合并等情况。完整的 body filter 逻辑较复杂代码见最后一节完整工程文件。6. 组合实战Header Body Filter 协同工作现在我们把两个过滤器合并到同一个模块。编译安装后每个响应都会自动添加三个安全头部。将响应体中出现的http://old.example.com替换为https://new.example.com或者其他自定义规则。这种协同模式在实际项目中非常有用例如反向代理透明注入在代理响应中添加追踪头header filter同时将页面中的外部图片链接替换为 CDN 地址body filter。老旧系统升级原服务无法修改通过 Nginx filter 自动添加缺失的安全头并进行字符串替换无需改动后端代码。7. 编译与测试7.1 编译假设 Nginx 源码位于/usr/local/src/nginx-1.24.0模块目录为~/myfilter。在模块目录中创建config文件ngx_addon_namengx_http_myfilter_module HTTP_FILTER_MODULES$HTTP_FILTER_MODULES ngx_http_myfilter_module NGX_ADDON_SRCS$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_myfilter_module.c进入 Nginx 源码目录运行./configure --add-module~/myfiltermakemakeinstall7.2 测试在nginx.conf中配置一个简单的静态服务器server { listen 80; server_name localhost; location / { root html; index index.html; } }在html/index.html中写入包含http://old.example.com的链接文本。启动 Nginx 后用curl -I查看响应头应包含新增的安全头再用curl http://localhost/查看响应体链接应被替换为 HTTPS 版本。HTTP/1.1 200 OK ... X-Content-Type-Options: nosniff X-Frame-Options: DENY X-XSS-Protection: 1; modeblock ...响应体中的http://old.example.com变为https://new.example.com。8. 完整工程文件为了方便读者直接上手以下提供完整的模块工程代码。新建目录ngx_http_myfilter_module将下面两个文件放入按上一节方法编译即可。8.1 config 文件ngx_addon_namengx_http_myfilter_module HTTP_FILTER_MODULES$HTTP_FILTER_MODULES ngx_http_myfilter_module NGX_ADDON_SRCS$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_myfilter_module.c8.2 ngx_http_myfilter_module.c#includengx_config.h#includengx_core.h#includengx_http.htypedefstruct{ngx_chain_t*last_buf;ngx_flag_tdone;}ngx_http_myfilter_ctx_t;staticngx_int_tngx_http_myfilter_header_filter(ngx_http_request_t*r);staticngx_int_tngx_http_myfilter_body_filter(ngx_http_request_t*r,ngx_chain_t*in);staticngx_int_tngx_http_myfilter_init(ngx_conf_t*cf);staticngx_http_output_header_filter_pt ngx_http_next_header_filter;staticngx_http_output_body_filter_pt ngx_http_next_body_filter;staticngx_http_module_tngx_http_myfilter_module_ctx{NULL,/* preconfiguration */ngx_http_myfilter_init,/* postconfiguration */NULL,/* create main conf */NULL,/* init main conf */NULL,/* create server conf */NULL,/* merge server conf */NULL,/* create location conf */NULL/* merge location conf */};ngx_module_tngx_http_myfilter_module{NGX_MODULE_V1,ngx_http_myfilter_module_ctx,/* module context */NULL,/* 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};staticngx_int_tngx_http_myfilter_init(ngx_conf_t*cf){ngx_http_next_header_filterngx_http_top_header_filter;ngx_http_top_header_filterngx_http_myfilter_header_filter;ngx_http_next_body_filterngx_http_top_body_filter;ngx_http_top_body_filterngx_http_myfilter_body_filter;returnNGX_OK;}staticngx_int_tngx_http_myfilter_header_filter(ngx_http_request_t*r){ngx_table_elt_t*h;if(r!r-main){returnngx_http_next_header_filter(r);}/* X-Content-Type-Options */hngx_list_push(r-headers_out.headers);if(hNULL){returnNGX_ERROR;}h-hash1;ngx_str_set(h-key,X-Content-Type-Options);ngx_str_set(h-value,nosniff);/* X-Frame-Options */hngx_list_push(r-headers_out.headers);if(hNULL){returnNGX_ERROR;}h-hash1;ngx_str_set(h-key,X-Frame-Options);ngx_str_set(h-value,DENY);/* X-XSS-Protection */hngx_list_push(r-headers_out.headers);if(hNULL){returnNGX_ERROR;}h-hash1;ngx_str_set(h-key,X-XSS-Protection);ngx_str_set(h-value,1; modeblock);returnngx_http_next_header_filter(r);}staticngx_int_tngx_http_myfilter_body_filter(ngx_http_request_t*r,ngx_chain_t*in){u_char*p,*new_buf;size_tlen;ngx_buf_t*b;ngx_chain_t*cl,*out,**ll;ngx_http_myfilter_ctx_t*ctx;ctxngx_http_get_module_ctx(r,ngx_http_myfilter_module);if(ctxNULL){ctxngx_pcalloc(r-pool,sizeof(ngx_http_myfilter_ctx_t));if(ctxNULL){returnNGX_ERROR;}ngx_http_set_ctx(r,ctx,ngx_http_myfilter_module);}if(ctx-done){returnngx_http_next_body_filter(r,in);}if(inNULL){ctx-done1;returnngx_http_next_body_filter(r,in);}/* 简单的替换http://old.example.com - https://new.example.com */outNULL;llout;for(clin;cl;clcl-next){bcl-buf;if(ngx_buf_special(b)){*llngx_alloc_chain_link(r-pool);if(*llNULL){returnNGX_ERROR;}(*ll)-bufb;(*ll)-nextNULL;ll(*ll)-next;continue;}pb-pos;lenb-last-b-pos;new_bufngx_palloc(r-pool,len);if(new_bufNULL){returnNGX_ERROR;}ngx_memcpy(new_buf,p,len);/* 简单替换使用穷举扫描这里只做一次 demo *//* 实际中可使用 ngx_http_sub_module 的复杂逻辑 */{u_char*snew_buf;u_char*endnew_buflen;while(send){if(ngx_strncasecmp(s,(u_char*)http://old.example.com,23)0){ngx_memcpy(s,https://new.example.com,24);s24;}else{s;}}}*llngx_alloc_chain_link(r-pool);if(*llNULL){returnNGX_ERROR;}(*ll)-bufngx_calloc_buf(r-pool);if((*ll)-bufNULL){returnNGX_ERROR;}(*ll)-buf-posnew_buf;(*ll)-buf-lastnew_buflen;(*ll)-buf-memory1;(*ll)-buf-temporary1;(*ll)-nextNULL;ll(*ll)-next;}returnngx_http_next_body_filter(r,out);}8.3 配置文件示例nginx.confworker_processes 1; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; server { listen 80; server_name localhost; location / { root html; index index.html; } } }以上工程直接编译即可生效。9. 总结本文从 Nginx HTTP Filter 的概念入手通过一个集 header filter 与 body filter 于一体的实战模块完整演示了开发流程。关键知识点包括Filter 链的插入机制与优先级。利用ngx_http_request_t的ctx数组在多次调用间保存上下文。Header filter 与 body filter 的函数签名与典型实现。编译、测试的完整步骤。掌握这些技能后你就可以自由扩展 Nginx 的功能无需修改上游服务实现安全加固、内容转换、数据脱敏等高级需求。完整的工程文件可直接用于生产环境前测试希望能为你的 Nginx 模块开发之旅提供有力指引。