在大型项目的部署中,经常会用到反向代理。
由于后续站点的后端部署就会涉及到反向代理,就研究了一下。
本文所提及的Web服务器软件只有Apache httpd(下文简写为Apache)、Nginx,因为我也只用过这两个。
以及,这是去年同期挖的一个坑,一直搁置了一年左右才想起来。
以及原本个人意向里这篇文章只会包含Apache的相关配置。
为何要使用反向代理
这一段的内容为我的个人见解。
- 相比于后端程序,Apach、Nginx这类Web服务器软件通常对http有着更好的支持和优化。
- 对于需要使用免费SSL证书的服务器,Apache、Nginx等服务器可以更好的处理免费SSL证书续期相关的配置。
- 对于单台服务器,若要同时部署大量后端程序,通过使用反向代理可让所有后端程序走80/443的标准http/https端口。
- 对于企业,通常会部署大量服务器,此时可以将几台服务器设置为网关服务器,暴露在公网,控制外网对内部服务器的访问。
- 如果部署了多台服务器,可以通过反向代理实现负载均衡,将流量平摊到各个服务器上。
Apache的反向代理配置
Apache如需支持反向代理,需要启用模块mod_proxy,以及你需要的代理模块。
mod_proxy_balancer: 负载均衡相关,等之后深入了解了会有用处。
假设我们将Spring Boot后端运行在8080端口上,我们想用api.local.azurezeng.com:443这个地址将后端程序暴露到外网。
接下来在Apache配置文件中启用需要的模块,然后添加下面的配置。
这里使用虚拟主机配置,以保证Apache服务器可以使用多个域名提供服务。
<VirtualHost *:443>
# ...
ServerName api.local.azurezeng.com
# 不要将服务器的/.well-known路径进行反代,方便处理SSL证书自动续期问题
ProxyPass /.well-known !
# 将其他所有请求反代到127.0.0.1:8080,也就是我们的后端端口
ProxyPass / http://127.0.0.1:8080/
# 保证部分传出外网的重定向请求,地址是正确的
ProxyPassReverse / http://127.0.0.1:8080/
# 将Host请求头发送给被反代的程序
ProxyPreserveHost On
# ...
</VirtualHost>
配置完成后重新加载Apache,来看看效果。我们的请求被全部重定向到了我们的后端服务上中。
Nginx的反向代理配置
本来说呢,这一段写不了的,因为当年开这个坑的时候本站服务器还在使用Apache作为Web服务器。今年暑假摸了一下Nginx,所以这一段也可以写了。
为了让php有更好的支持,我把php和nginx都安装到了Linux虚拟机下。
根据个人配置,相对于虚拟机而言,我们主机上运行的后端服务器的地址就变为 了10.1.10.1:8080。我们现在想使用api.vm.azurezeng.com:443(此域名已提前指向虚拟机)将后端程序暴露到公网中。
需要注意,下面的配置需要放到配置文件的http设置节点中。
同样的,这个配置中我们也使用了虚拟主机功能来保证nginx能处理多个域名下的请求。
http {
#...
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name api.vm.azurezeng.com;
index index.html index.htm default.html default.htm;
root /home/wwwroot/public_html/api; # 自行更改
#...
# 排除/.well-known文件夹,以方便SSL证书自动续期
location ^~ /.well-known/ {
allow all;
}
location / {
proxy_pass http://10.1.10.1:8080/;
}
#...
}
#...
}
输入完配置后重新加载nginx,此时就可以用这个地址访问到我们的后端服务了。
如何让后端程序正确的识别到用户IP地址
相关原理和配置方案
通过后端程序获取请求来源的IP地址原理可知,使用上面的配置文件配置好反代之后,后端程序已经无法正常的识别到请求来源的IP地址。
此时需要了解一些东西,来保证后端程序能正确的在反向代理的环境下工作。
X-Forwarded-For: 表示请求来源IP地址,以及经过的一系列代理服务器的IP地址。
X-Forwarded-Port: 用的较少,用于指示请求来源连接代理服务器,所使用的代理服务器端口。
X-Forwarded-Host: 表示请求来源向代理服务器发起本次请求所使用的域名,常用于使用一个服务器处理多个域名下的请求的场景。
X-Forwarded-Proto: 表示请求来源与代理服务器之间的连接所使用的协议(如http或https)。
其中最重要的请求头就是X-Forwarded-For,我们需要用它来获取请求来源的IP地址。
Apache的请求头设置
Apache在向后端程序发起请求前,已经自动设置好了相关请求头。当然,除非你手动关闭掉了这个功能(ProxyAddHeaders Off
)。
写一个接口来看看发送过来的请求头是什么情况:
Nginx的请求头配置
而对于Nginx,我们需要做一些额外设置。
location / {
proxy_pass http://10.1.10.1:8080/;
proxy_redirect http://10.1.10.1:8080/ /;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
$proxy_add_x_forwarded_for
这里需要提醒的一点是,如果传过来的请求已经存在X-Forwarded-For
,则会将代理服务器的IP地址追加到发往后端服务器的X-Forwarded-For
请求头,否则就等同于请求来源的IP地址。在需要多层反代的情况下$proxy_add_x_forwarded_for
会非常有用,但是也存在一些问题(下面会提)。
设置完成之后重新加载nginx,就可以正常的检测到X-Forwarded-For
相关的请求头了。
此时后端程序就可以通过X-Forwarded-For
正常的获取请求来源的IP地址了。
对后端程序进行设置调整
相比于直接从请求头直接读取请求的IP地址,若能直接用后端提供的API来读取请求的IP地址会好很多。大部分后端框架和后端程序对X-Forwarded-For提供了较好的支持。
下面就提几个我用过的后端框架和服务器程序的相关配置,其他的框架和软件请自行阅读相关文档。
在做好相关设置的前提下,框架的访问来源IP地址相关API,或者程序的访问来源IP地址识别就不会发生问题了。
Spring Boot
前往application.properties或application.yml,加入下面的配置(下面使用YAML配置)。
server:
# 这里正常情况下,用framework也可以,反正不能是默认的none
forward-headers-strategy: native
Spring Cloud Gateway
这玩意本身就是个网关服务器,将请求发往下级后端程序。所以默认设置已经支持X-Forwarded-For转发。
需要详细设置请参阅相关的配置文档。
当然,通常情况不建议将Spring Cloud Gateway网关程序直接暴露到公网,而是套一层Apache或nginx等这类Web程序,用反代访问Spring Cloud Gateway网关程序。
ASP.NET Core/Kestrel
此处使用ASP.NET Core 6.0举例。
找到程序代码的Main函数(或者别的地方,如果你把这段初始化代码扔到别的地方去了),加入下面的代码。
var builder = WebApplication.CreateBuilder(args);
//..
var app = builder.Build();
//...
app.UseForwardedHeaders(new ForwardedHeadersOptions {
// 此处可灵活调整
// 你也可以仅使用X-Forwarded-For和X-Forwarded-Proto
// 如果后端程序和Web程序不在一个服务器上,
// 可能需要调整KnownNetworks和KnownProxies
ForwardedHeaders = ForwardedHeaders.All,
KnownNetworks = { new IPNetwork(IPAddress.Parse(10.255.255.255), 8) }
});
Tomcat
找到conf/server.xml
文件,依次按照Server
->Service
->Engine
->Host
选项,然后加入下面的内容。
<!-- RemoteIp Value -->
<Valve className=org.apache.catalina.valves.RemoteIpValve />
IP伪造问题和解决办法
我们现在已经知道,后端程序在使用反向代理之后需要依赖X-Forwarded系列请求头来获取请求来源的IP地址。目前我们通过配置Apache/Nginx,做到了让Apache/Nginx在将请求发往后端程序时自动设置好相关的请求头。
但是,目前的配置存在安全隐患。如果请求发送过来的时候就带有X-Forwarded相关请求头,里面有伪造的请求来源IP地址,Apache/Nginx会误认为发送过来的请求已经经过了一次反向代理,此时后端程序收到的X-Forwarded请求头信息就是被伪造过的。换句话说,我们如果知道有这个漏洞,就可以随意的伪造IP地址。
虽然部分后端有“信任的反代服务器”相关的设置,能一定程度的规避此问题,但这只是治标不治本。
对于暴露在外网的服务器(如果是内网服务器继续进行反代,请直接使用上面的配置,否则可能出现请求头出问题),建议在将请求传递到下一级内网服务器或后端服务时,清除请求的X-Forwarded相关请求头,再由Web服务器设置X-Forwarded相关请求头内容。
不要信任用户的任何输入。——前端设计名言
对于Apache,我们可以修改一下配置,来修复此问题。(需要使用模块mod_headers)
RequestHeader指令与Apache的反向代理自动处理X-Forwarded请求头选项(ProxyAddHeaders
)存在矛盾,因此必须关闭ProxyAddHeaders
并自行处理请求头内容。
<VirtualHost *:443>
#...
ServerName api.local.azurezeng.com
#...
ProxyPass /.well-known !
ProxyPass / http://127.0.0.1:8080/
ProxyPassReverse / http://127.0.0.1:8080/
# 永远不要相信用户的输入,删除请求中的X-Forwarded相关信息
RequestHeader unset X-Forwarded-*
ProxyAddHeaders Off
# 手动设置
RequestHeader set X-Forwarded-For expr=%{REMOTE_ADDR}
RequestHeader set X-Forwarded-Port expr=%{SERVER_PORT}
RequestHeader set X-Forwarded-Host expr=%{SERVER_NAME}
RequestHeader set X-Forwarded-Proto expr=%{REQUEST_SCHEME}
ProxyPreserveHost On
#...
</VirtualHost>
对于Nginx,则是将上面提到的$proxy_add_x_forwarded_for
替换为$remote_addr
。
当请求来源没有携带X-Forwarded-For
时,$proxy_add_x_forwarded_for
的值等效于$remote_addr
(请求来源的IP地址)。做此修改之后,反向代理服务器就不会再读取来源的X-Forwarded-For请求头,而是将请求来源的IP地址作为X-Forwarded-For的内容,发往下级服务器。
做完上述修改,并重新加载服务器配置之后,向服务器发送的X-Forwarded相关内容就会被服务器直接丢弃,也就不存在X-Forwarded伪造问题了。
总结
至此,我们就设置好了基础的Web服务器的反向代理,已经可以用反向代理处理一些比较常见的情景了。
我个人觉得,本文只做抛砖引玉作用,向各位读者指明一些问题的解决方向,以及踩到的一些坑。
如果还需要做一些更高级的配置,还需要读者自己自行研究相关配置,来做到符合自己需求。