记一次由gzip引起的nginx转发事故

故事背景

书接前几篇文章,仍然是交付甲方遇到的一个特殊诉求,从而引发了本期的事故。甲方的诉求是前端的请求过来,需要加密,但是要经过waf,必须要求是请求明文,那就要在waf和nginx之间做一个解密前置应用处理。大致架构图如下:
在这里插入图片描述
本次事故的起因是因为,经过waf的请求响应头信息增加了一个Content-Encoding:gzip导致的数据无法返回前端。

技术栈

nginx:1.16.1
springboot:2.5.14
hutool:5.8.15

NGINX下载:NGINX下载链接

情景再现

我们一点一点还原下,当时遇到的问题。我们这里需要两个java应用和一个nginx三个工程。注意下述不是完整代码,都是核心代码片段。只为说明问题产生的过程,所以不会大面积贴出所有代码!!!

前置服务

http调用目标服务核心代码

注意:此处代码重点在于使用hutool HttpRequest.execute()调用目标服务,其余方法理性观看!

public static HttpResponse executeAndGetResponse(HttpServletRequest request, String forwardAddr, String decryptData, String wafHost) {if (decryptData == null) {return executeAndGetResponse(request, forwardAddr, wafHost);}HttpRequest httpRequest = getHttpRequest(request, forwardAddr, wafHost);Map<String, Object> copyForm = null;if (httpRequest.form() != null) {copyForm = new HashMap<>(httpRequest.form());}httpRequest.body(decryptData, request.getContentType());//重新设置form,设置body时会将form设为null,所以需重新设置form//TODO body和form二者只能存在一个,当form存在时,则body会被置为null,除了Get请求其他参数都要放到body体中if (Objects.equals(RequestMethodEnum.GET.getMethod(), request.getMethod())&& httpRequest.form() == null && CollectionUtil.isNotEmpty(copyForm)) {httpRequest.form(copyForm);}HttpResponse response = httpRequest.execute();return response;}

响应流返回给ng核心代码

public static void returnStream(HttpResponse forwardResponse, HttpServletResponse response) {//拷贝转发响应头携带的信息Map<String, List<String>> headers = forwardResponse.headers();if (CollectionUtil.isNotEmpty(headers)) {log.info("----------------------------------------------");for (Map.Entry<String, List<String>> entry : headers.entrySet()) {if (CollectionUtil.isNotEmpty(entry.getValue())) {response.addHeader(entry.getKey(), entry.getValue().get(0));log.info("响应头信息:[{}:{}]", entry.getKey(), entry.getValue().get(0));} else {response.addHeader(entry.getKey(), "");}}}//输出响应日志String contentType = forwardResponse.header("Content-Type");if (StringUtils.isNotBlank(contentType) && contentType.toLowerCase().startsWith(MediaType.APPLICATION_JSON_VALUE)) {String body = forwardResponse.body();if (body.length() > 1000) {log.info("请求响应长度大于1000,只打印前1000个字符,详情可查看转发服务:{}", StrUtil.cleanBlank(body).substring(0, 1000));} else {log.info("请求响应:{}", StrUtil.cleanBlank(body));}}OutputStream outputStream = null;GZIPOutputStream gzipOut = null;try {response.setCharacterEncoding("UTF-8");outputStream = response.getOutputStream();IoUtil.copy(forwardResponse.bodyStream(), outputStream);outputStream.flush();} catch (IOException e) {log.error("流读取IO异常", e);throw new RuntimeException(e);} finally {if (gzipOut != null) {try {gzipOut.close();} catch (IOException e) {throw new RuntimeException(e);}}if (outputStream != null) {try {outputStream.close();} catch (IOException e) {throw new RuntimeException(e);}}}}

启动配置文件

spring.application.name=router
server.port=7070waf.host=127.0.0.1
forward.url=http://${waf.host}:9090

目标服务

接口实现

import cn.hutool.core.io.IoUtil;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.GZIPOutputStream;@RestController
@RequestMapping("/hello")
@Slf4j
public class HelloController {@GetMapping("/test")public Map getTest() {Map<String, String> ng = new HashMap<>();ng.put("2", "333333333333333333333");return ng;}@GetMapping("/test3")public void getTest3(String a, String b, HttpServletResponse response) {Map<String, String> ng = new HashMap<>();ng.put("2", "Hello,received:" + a + b);response.addHeader("Content-Encoding", "gzip");OutputStream outputStream = null;GZIPOutputStream gzipOut = null;try {response.setCharacterEncoding("UTF-8");outputStream = response.getOutputStream();log.info("--响应开始压缩--");gzipOut = new GZIPOutputStream(outputStream);IoUtil.write(gzipOut, false, JSON.toJSONBytes(ng));gzipOut.finish();log.info("--响应压缩完成--");outputStream.flush();} catch (IOException e) {log.error("流读取IO异常", e);throw new RuntimeException(e);} finally {if (gzipOut != null) {try {gzipOut.close();} catch (IOException e) {throw new RuntimeException(e);}}if (outputStream != null) {try {outputStream.close();} catch (IOException e) {throw new RuntimeException(e);}}}}@GetMapping("/test2")public Map getTest2() {Map<String, String> ng = new HashMap<>();ng.put("2", "333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333");ng.put("3", "333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333");return ng;}@GetMapping("/gzip-test")public void gzipTest(HttpServletRequest request, HttpServletResponse response) {response.addHeader("Content-Encoding", "gzip");OutputStream outputStream = null;GZIPOutputStream gzipOut = null;try {response.setCharacterEncoding("UTF-8");outputStream = response.getOutputStream();log.info("--响应开始压缩--");gzipOut = new GZIPOutputStream(outputStream);Map<String, String> data = new HashMap<>();data.put("name", "Kevin");IoUtil.write(gzipOut, false, JSON.toJSONBytes(data));gzipOut.finish();log.info("--响应压缩完成--");outputStream.flush();} catch (IOException e) {log.error("流读取IO异常", e);throw new RuntimeException(e);} finally {if (gzipOut != null) {try {gzipOut.close();} catch (IOException e) {throw new RuntimeException(e);}}if (outputStream != null) {try {outputStream.close();} catch (IOException e) {throw new RuntimeException(e);}}}}@GetMapping("/gzip-test2")public void gzipTest2(HttpServletRequest request, HttpServletResponse response) {response.addHeader("Content-Encoding", "gzip");OutputStream outputStream = null;GZIPOutputStream gzipOut = null;try {response.setCharacterEncoding("UTF-8");outputStream = response.getOutputStream();log.info("--响应开始压缩--");gzipOut = new GZIPOutputStream(outputStream);Map<String, String> data = new HashMap<>();data.put("name", "Mary");IoUtil.write(gzipOut, false, JSON.toJSONBytes(data));gzipOut.finish();log.info("--响应压缩完成--");outputStream.flush();} catch (IOException e) {log.error("流读取IO异常", e);throw new RuntimeException(e);} finally {if (gzipOut != null) {try {gzipOut.close();} catch (IOException e) {throw new RuntimeException(e);}}if (outputStream != null) {try {outputStream.close();} catch (IOException e) {throw new RuntimeException(e);}}}}}

启动配置文件

spring.application.name=demo2
server.port=9090

nginx配置


#user  nobody;
worker_processes  1;error_log  logs/error.log;
#error_log  logs/error.log  notice;
error_log  logs/error.log  info;pid        logs/nginx.pid;events {worker_connections  1024;
}http {include       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  logs/access.log  main;sendfile        on;#tcp_nopush     on;keepalive_timeout  65;upstream backend {server 127.0.0.1:7070;keepalive 50;}#gzip  on;server {listen       6060;server_name  127.0.0.1;access_log  logs/host.access.log  main;client_max_body_size 20m;client_header_buffer_size 32k;location / {proxy_pass http://backend;#proxy_http_version 1.1;#proxy_set_header Connection "";}}}

问题集锦

错误1:NS_ERROR_NET_RESET

在这里插入图片描述

解决方案

增加配置
proxy_http_version 1.1;
proxy_set_header Connection “”;


#user  nobody;
worker_processes  1;error_log  logs/error.log;
#error_log  logs/error.log  notice;
error_log  logs/error.log  info;pid        logs/nginx.pid;events {worker_connections  1024;
}http {include       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  logs/access.log  main;sendfile        on;#tcp_nopush     on;keepalive_timeout  65;upstream backend {server 127.0.0.1:7070;keepalive 50;}#gzip  on;server {listen       6060;server_name  127.0.0.1;access_log  logs/host.access.log  main;client_max_body_size 20m;client_header_buffer_size 32k;location / {proxy_pass http://backend;proxy_http_version 1.1;proxy_set_header Connection "";}}}
错误2:net::ERR_CONTENT_DECODING_FAILED 200 (OK)

在这里插入图片描述

解决方案

修改前置应用返回ng的响应流处理方法,检测到响应头中含有Content-Encoding:gzip对报文内容做压缩处理,再返给ng

public static void returnStream(HttpResponse forwardResponse, HttpServletResponse response) {//拷贝转发响应头携带的信息Map<String, List<String>> headers = forwardResponse.headers();if (CollectionUtil.isNotEmpty(headers)) {log.info("----------------------------------------------");for (Map.Entry<String, List<String>> entry : headers.entrySet()) {if (CollectionUtil.isNotEmpty(entry.getValue())) {response.addHeader(entry.getKey(), entry.getValue().get(0));log.info("响应头信息:[{}:{}]", entry.getKey(), entry.getValue().get(0));} else {response.addHeader(entry.getKey(), "");}}}//输出响应日志String contentType = forwardResponse.header("Content-Type");if (StringUtils.isNotBlank(contentType) && contentType.toLowerCase().startsWith(MediaType.APPLICATION_JSON_VALUE)) {String body = forwardResponse.body();if (body.length() > 1000) {log.info("请求响应长度大于1000,只打印前1000个字符,详情可查看转发服务:{}", StrUtil.cleanBlank(body).substring(0, 1000));} else {log.info("请求响应:{}", StrUtil.cleanBlank(body));}}OutputStream outputStream = null;GZIPOutputStream gzipOut = null;try {response.setCharacterEncoding("UTF-8");outputStream = response.getOutputStream();String contentEncoding = forwardResponse.contentEncoding();if(StringUtils.isNotBlank(contentEncoding) && contentEncoding.equalsIgnoreCase("gzip")) {log.info("--响应开始压缩--");gzipOut = new GZIPOutputStream(outputStream);IoUtil.write(gzipOut, false, forwardResponse.bodyBytes());gzipOut.flush();gzipOut.finish();log.info("--响应压缩完成--");} else {IoUtil.write(outputStream, false, forwardResponse.bodyBytes());outputStream.flush();}} catch (IOException e) {log.error("流读取IO异常", e);throw new RuntimeException(e);} finally {if (gzipOut != null) {try {gzipOut.close();} catch (IOException e) {throw new RuntimeException(e);}}if (outputStream != null) {try {outputStream.close();} catch (IOException e) {throw new RuntimeException(e);}}}}

高版本Nginx

最开始我本地复现并没有关注到ng的版本,所以开始使用的是1.25.3的版本,还衍生出了新的问题也贴出来供大家参考下。


#user  nobody;
worker_processes  1;error_log  logs/error.log;
error_log  logs/error.log  notice;
error_log  logs/error.log  info;pid        logs/nginx.pid;events {worker_connections  10000;
}http {include       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  logs/access.log  main;sendfile        on;tcp_nopush     on;keepalive_timeout  65;gzip  on;upstream backend {server 127.0.0.1:7070;keepalive 50;}server {listen       6060;server_name  127.0.0.1;access_log  logs/host.access.log  main;client_max_body_size 20m;client_header_buffer_size 32k;location / {proxy_pass http://backend;proxy_http_version 1.1;proxy_set_header Connection "";}# redirect server error pages to the static page /50x.html#error_page   500 502 503 504  /50x.html;location = /50x.html {root   html;}}}

错误1:502 Bad Gateway

在这里插入图片描述

解决方案

通过配置ng的错误日志可以查到如下错误:

2024/03/28 01:16:07 [error] 8352#5480: *43 upstream sent duplicate header line: "Transfer-Encoding: chunked", previous value: "Transfer-Encoding: chunked" while reading response header from upstream, client: 127.0.0.1, server: 127.0.0.1, request: "GET /noauth/captcha/slide-image HTTP/1.1", upstream: "http://127.0.0.1:7070/noauth/captcha/slide-image", host: "localhost:6060"

意思是请求头中行Transfer-Encoding: chunked重复导致的;
这个只需要在后端服务中返回ng之前移除请求头就可以了。
注意:这是因为proxy_http_version 1.1;
proxy_set_header Connection “”; 这两个配置导致的,高版本nginx中会自动加入Transfer-Encoding: chunked,所以从后端传过来的response中也存在就会重复,nginx1.16.1版本就不会出现这个问题。
在这里插入图片描述

总结

一个gzip引发的案件,原因是因为过waf的时候,waf会自动引入gzip压缩处理,导致前置应用没有处理,解决此问题的方案有2。

方案一

前置应用获取到目标服务的响应结果后,已经是解压后的数据,这是因为hutool是一个http客户端,如果服务端返回的response中带有gzip的标志,hutool获取到的结果已经是解压过后的数据,可以继续移除hutool获取到的响应头中的Content-Encoding:gzip往外继续抛即可,这样抛到ng的时候也是不带gzip头信息的,数据也刚好搭对。但是注意这就失去了压缩的意义了,会损失一些传输损耗,达不到压缩的积极意义。

方案二

那就是检测到响应头中带有gzip标识,返回响应流的时候,做压缩处理,同样是响应头和响应体搭对即可。

未解之谜

情景复现中难以模拟我们真实的一种场景是,我们做了压缩处理,但是在请求中只有一个接口是成功的,其余接口均无返回,我们也不得其解,如果能找到原因再来公布。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/573143.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

搜索插入位置-java

题目描述 : 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会被按顺序插入的位置。请必须使用时间复杂度为 O(log n) 的算法。 思路分析: 这段代码的解题思想是利用二分查找的方法在…

MySql实战--事务到底是隔离的还是不隔离的

第3篇文章和你讲事务隔离级别的时候提到过&#xff0c;如果是可重复读隔离级别&#xff0c;事务T启动的时候会创建一个视图read-view&#xff0c;之后事务T执行期间&#xff0c;即使有其他事务修改了数据&#xff0c;事务T看到的仍然跟在启动时看到的一样。也就是说&#xff0c…

深入探讨分布式ID生成方案

&#x1f388;&#x1f388;作者主页&#xff1a; 喔的嘛呀&#x1f388;&#x1f388; &#x1f388;&#x1f388;所属专栏&#xff1a;python爬虫学习&#x1f388;&#x1f388; ✨✨谢谢大家捧场&#xff0c;祝屏幕前的小伙伴们每天都有好运相伴左右&#xff0c;一定要天天…

OSPF-区域间路由计算

一、概述 前面学习了我们学习了Router-LSA和Network-LSA&#xff0c;它们都只能在区域内进行泛洪&#xff0c;而且我们之前一直主要是单区域学习。OSPF的核心是骨干区域Area 0&#xff0c;其它都为非骨干区域。但是在大型网络中&#xff0c;单区域OSPF会存在一定的问题&#xf…

HCIA-Datacom实验_03_实验一:华为VRP系统基本操作

1.运行eNSP&#xff0c;设置-界面设置-自定义界面-设备标签&#xff0c;“总显示接口标签” 打钩。 2.按照实验拓扑添加设备 注&#xff1a;如果是真实环境&#xff0c;需要接两条线&#xff1a; &#xff08;1&#xff09;串口线&#xff1a;电脑USB口到网络设备Console口&am…

EdgeGallery开发指南

API接口 简介 EdgeGallery支持第三方业务系统通过北向接口网关调用EdgeGallery的业务接口。调用流程如下图所示&#xff08;融合前端edgegallery-fe包含融合前端界面以及北向接口网关功能&#xff0c;通过浏览器访问时打开的是融合前端的界面&#xff0c;通过IP:Port/urlPref…

免费SSL证书和付费SSL证书的区别点

背景&#xff1a; 在了解免费SSL证书和付费SSL证书的区别之前&#xff0c;先带大家了解一下SSL证书的概念和作用。 SSL证书的概念&#xff1a; SSL证书就是基于http超文本传输协议的延伸&#xff0c;在http访问的基础上增加了一个文本传输加密的协议&#xff0c;由于http是明…

第十篇【传奇开心果系列】Python自动化办公库技术点案例示例:深度解读Python自动化操作Excel

传奇开心果博文系列 系列博文目录Python自动化办公库技术点案例示例系列博文目录 前言一、重要作用解说二、Python操作Excel的常用库介绍三、数据处理和分析示例代码四、自动化报表生成示例代码五、数据导入和导出示例代码六、数据可视化示例代码八、数据校验和清洗示例代码九、…

【Java程序设计】【C00386】基于(JavaWeb)Springboot的校运会管理系统(有论文)

基于&#xff08;JavaWeb&#xff09;Springboot的校运会管理系统&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 博主介绍&#xff1a;java高级开发&#xff0c;从事互联网行业六年&#xff0c;已经做了六年的毕业设计程序开发&#xff0c;开发过上…

【学习心得】Jupyter常用操作与魔法方法

一、安装与打开 Jupyter是什么我就不啰嗦了&#xff0c;直接安装&#xff1a; pip install jupyter 安装完后&#xff0c;在你想要打开的项目路径下&#xff0c;唤出CMD执行下面命令就可以使用jupyter notebook了 jupyter notebook 也可以用更加好用的jupyter lab&#xff0…

LabVIEW无人机大气数据智能测试系统

LabVIEW无人机大气数据智能测试系统 随着无人机技术的迅速发展&#xff0c;大气数据计算机作为重要的机载设备&#xff0c;在确保飞行安全性方面发挥着重要作用。设计了一套基于LabVIEW的无人机大气数据智能测试系统&#xff0c;通过高效、稳定的性能测试&#xff0c;及时发现…

Memcached非关系型数据库介绍

使用背景 Memcached 不是一个数据库&#xff0c;而是一个高性能的分布式内存对象缓存系统。它主要用于减轻数据库负载&#xff0c;提高动态Web应用的速度、可扩展性和性能。Memcached 的工作原理是将数据存储在内存中&#xff0c;以提供快速的数据访问。当应用程序需要访问数据…