全站https发现的spring gateway header覆盖(bug?)问题
1. 需求
项目环境准备换成https环境,http也需要保留,两种方式都可以访问
某些地址需要进行双向认证
当前环境结构大概如下:
nginx(http) -> nginx(http) -> gateway(http) -> tomcat(http);
改变后的结构:
nginx(https,http) -> nginx(http) -> gateway(http) -> tomcat(http);
2.配置 https
这种配置网上有很多,这里不详细说明,配置大概如下:
listen 80 default;
listen 443 ssl;
ssl_certificate ssl.crt;
ssl_certificate_key ssl.key;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host:$server_port;
proxy_pass http://endpoint:8084;
index index.html;
}
---
配置完成,访问系统也没问题
3.问题
当后端出现进行Redirect 重定向跳转 301,302 时,地址就会变得不对,比如本来是访问的https,结果跳转到http页面
启动项目(tomcat)进行调试,发现request.getScheme() 拿到的竟然是 http,这不正常啊,明明已经设置了 X-Forwarded-Proto
再进行调试,发现 header 中竟然没有 X-Forwarded-Proto ,想到是不是上层(gateway)是不是拦截,没有传递
随即调试 gateway, 发现在 NettyRoutingFilter 中,被覆盖了!!!
目前使用的gateway版本(spring-cloud-gateway-core.2.1.2),与最新的代码进行了对比,该代码也有太大变化
随即反复读了源码,还是找不到能友好的重写与修复的方法
github 也提交了问题,但过去几天也没人理,而且还发现有其它人出现跟我一样的问题;
https://github.com/spring-cloud/spring-cloud-gateway/issues/1786
4.奇怪现象
gateway 不是不传递 X-Forwarded-Proto,而是因为 X-Forwarded-Proto 与 X-Forwarded-For 计算出的 hashCode(?) 一致,导致相互被覆盖
这真的是一个有趣的问题(BUG),规则大概如下:
X-Forwarded-For, X-Forwarded-Proto : 前者覆盖后者
X-Forwarded-For, x-forwarded-proto : 后者覆盖前者
大概好像是因为(猜测),小写的header在map中比大写的排在后面,然后在遍历的时候后面的覆盖了前面的
NettyRoutingFilter 中创建转换 DefaultHttpHeaders (netty) 时,因为两个header计算出来的hashCode一致,就被覆盖了
---
通过查看源码,发现好像DefaultHttpHeaders控制了header个数,最多只传递16个
而且这个数量在代码中是写死的,无法通过参数等形式传入
---
不理解一个这么简单的逻辑会有这种实现方式(摊手),如果有人知道的话也烦请告诉一声
5.过程
经过各种测试,比如
自定义header,声明一个 my-forwarded-proto,发现这个被丢弃,无法传递到业务(tomcat);
只传 x-forwarded-proto, 业务获取的scheme还是 http。 查看header,x-forwarded-proto有被正确传递(value: https,http)。
再进一步查看,发现tomcat获得scheme的逻辑是: 只要其中有一个不是https,scheme就不是https;
想将gateway切换到tomcat容器,也因为各种配置问题无法进行
6.解决
也不是解决,对于上面的问题(gateway bug?)找不到解决方案,死磕问题几天,又没有进展
最终通过 nginx 的 proxy_redirect 重新对路径进行了跳转:
如果后端返回了当前系统的路径,然后重写路径,将scheme进行替换,达到各环境(http,https)的统一
proxy_redirect ~^http://ip/(.*) $scheme://ip/$1;