业务中有一个通过识别访问端IP限制接口访问的场景。这里提供两种实现方式参考:
1. 通过nginx配置允许的ip段,nginx配置参考如下:
server {
listen 80;
server_name local.fatwo.cn;
access_log /usr/local/etc/nginx/logs/local.fatwo.cn.access.log main;
location / {
proxy_pass http://127.0.0.1:8080/;
proxy_redirect off;
# 这里的匹配规则是从上往下,匹配到则会马上跳出,忽略后续的规则。
allow 192.168.100.0/30;#允许ip段,all为所有 这里允许访问的IP范围为:192.168.100.1~192.168.100.3
deny all; #禁用ip段,all为所有
}
}
-
缺点:业务开发不好灵活配置nginx的修改,变更需要通知运维处理(开发即运维除外)
-
通过这种方式实现,去访问对应服务,若不在IP允许范围段内,会提示对应状态码:403 forbidden
2. 在业务层代码实现一个IP限制拦截器:通过获取客户端的IP地址,使用正则表达式匹配对应IP规则限制访问。
这里实现,需要能在业务服务上正确获取客户端IP地址,这就需要依靠代理层透传对应的真实IP。
在Nginx里有以下变量可以获取访问端IP:
- remote_addr
代表上一层客户端的IP,但它的值不是由客户端提供的,而是服务端根据上一层客户端的IP自动识别的,所以一般是不可伪造的。
-缺点:现在的业务服务一般都会经过多层代理转发,所以通过这个值获取到的IP一般是就近的代理服务,是不符合预期的。
- X-Real-IP
这是一个自定义头部字段,通常被 HTTP 代理用来表示与它产生 TCP 连接的设备 IP,这个设备可能是其他代理,也可能是真正的请求端,这个要看经过代理的层级次数或是是否始终将真实IP一路传下来。
要让这个值是真实的用户IP,需要第一层代理获取到真实客户端IP,然后每一层代理透传下去,直到最后一层业务服务端。
nginx的首层代理的配置参考如下:
server {
***(此处省略其他配置)
location / {
***(此处省略其他配置)
proxy_set_header x-real-ip $remote_addr;
}
}
nginx的中间层代理的配置参考如下(也可以不配置x-real-ip的header,正常境况会默认透传):
server {
***(此处省略其他配置)
location / {
***(此处省略其他配置)
proxy_set_header x-real-ip $http_x_real_ip;
}
}
-缺点:当有多层代理或使用CDN时,如果代理服务器不把用户的真实IP传递下去,那么业务服务器将获取不到用户的真实IP
- X-Forwarded-For(简称XFF)
X-Forwarded-For 是一个 HTTP 扩展头部。HTTP/1.1(RFC 2616)协议并没有对它的定义,它最开始是由 Squid 这个缓存代理软件引入,用来表示 HTTP 请求端真实 IP。如今它已经成为事实上的标准,被各大 HTTP 代理、负载均衡等转发服务广泛使用,并被写入 RFC 7239(Forwarded HTTP Extension)标准之中。
其格式为:
X-Forwarded-For: client, proxy1, proxy2
# 假设这个IP链没有被中断或伪造,这里第一个IP即为用户的真实IP,最后一个即为靠近业务服务的代理层IP
在nginx中配置参考如下:
server {
***(此处省略其他配置)
location / {
***(此处省略其他配置)
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
-缺点:通过这个变量获取到的IP是可以被伪造的。如何伪造在后面的实验可以找到答案。
- 当然以上的参数已经可以满足想要获取真实用户IP的场景,不过随着微服务化的兴起,我们现在的服务一般会经过层层代理转发,IP链中会存在代理层、CDN等不需要关注的IP,可以通过Nginx自带的real_ip_header模块维护过滤这些IP
server {
***(此处省略其他配置)
location / {
***(此处省略其他配置)
set_real_ip_from 192.168.100.0/30; # 设置可信任的IP地址,这些IP可以是常用的CDN白名单,设置后可以避免影响真实IP的获取
real_ip_header X-Forwarded-For; # 定义从哪个请求头获取IP信息,其值将用于替换客户端地址。一般都使用X-Forwarded-For
real_ip_recursive on; # 是否启用递归搜索。启用则会对可信任的IP进行匹配,客户端IP链会检查所有IP,过滤所有可信任IP,剩下不可信任IP。禁用则只检查IP链最后一个地址过滤。
}
}
使用docker模拟环境做个实验验证下自己的思考。
通过这个实验,我们能收获什么?
- 如何通过增加nginx代理,合理绕过IP限制能访问服务。
- 如何伪造客户端真实IP,让业务服务端获取不到正确的用户IP地址。
下图是需要模拟环境(基础环境配置准备)的整体概览供参考
基础镜像准备:nginx1、nginx2、php-fpm-nginx
# 通过docker镜像仓库拉最新的nginx镜像包,这个包是一个仅安装了nginx的linux系统镜像(很多常用命令工具是没有安装的,可以用apt-get去安装)
docker pull nginx:lastest
# 启动一个镜像名为nginx1的nginx镜像,端口映射8081到容器上的80端口:容器IP是192.168.100.1
docker run --name nginx1 -p 8081:80 -d nginx
# 启动一个镜像名为nginx2的nginx镜像,端口映射8082到容器上的80端口:容器IP是192.168.100.2
docker run --name nginx2 -p 8082:80 -d nginx
# 通过docker镜像仓库下载php-fpm镜像,我这里下载的是:php:7.3-fpm(同样可以通过apt-get安装一些需要的工具:比如nginx)
docker pull php:7.3-fpm
# 启动一个镜像名为php-fpm-nginx的镜像,容器IP是192.168.100.3
docker run --name php-fpm-nginx -p 9002:9000 -d php:7.3-fpm
# 进入启动的镜像:php-fpm-nginx 安装nginx
docker exec -it 527948a0956d bash
apt-get update # 先更新apt-get
apt-get install nginx # 安装nginx
apt-get install vim # 安装vim用于修改配置文件
nginx # 启动nginx服务
环境配置准备
- nginx环境(IP:172.20.222.65 && 192.168.100.5):模拟外网访问
# 新建文件:/etc/nginx/conf.d/1.fatwo.cn.conf 配置如下:
server {
listen 80;
server_name 1.fatwo.cn;
access_log /usr/local/etc/nginx/logs/1.fatwo.cn.access.log main;
location / {
proxy_pass http://localhost:8081/;
proxy_redirect off;
proxy_set_header Cookie $http_cookie;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
# 新建文件:/etc/nginx/conf.d/2.fatwo.cn.conf 配置如下:
server {
listen 80;
server_name 2.fatwo.cn;
access_log /usr/local/etc/nginx/logs/2.fatwo.cn.access.log main;
location / {
proxy_pass http://localhost:8082/;
proxy_redirect off;
proxy_set_header Cookie $http_cookie;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
- nginx1环境(Host: 1.fatwo.cn && IP:192.168.100.1):IP限制访问,转发到php业务服务
# 新建文件:/etc/nginx/conf.d/1.fatwo.cn.conf 配置如下:
server {
listen 80;
server_name 1.fatwo.cn;
access_log /etc/nginx/logs/1.fatwo.cn.access.log main;
location / {
proxy_pass http://local.fatwo.cn/;
proxy_redirect off;
proxy_set_header Cookie $http_cookie;
proxy_set_header x-real-ip $http_x_real_ip;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
allow 192.168.100.0/30;
deny all;
}
}
- nginx2环境(Host:2.fatwo.cn && IP:192.168.100.2):一台在IP允许范围内的服务,用于绕过IP限制访问我们想要访问的服务1.fatwo.cn
# 新建文件:/etc/nginx/conf.d/2.fatwo.cn.conf 配置如下:
server {
listen 80;
server_name 2.fatwo.cn;
access_log /etc/nginx/logs/2.fatwo.cn.access.log main;
location / {
proxy_pass http://192.168.100.1/;
proxy_redirect off;
proxy_set_header Host "1.fatwo.cn";
proxy_set_header Cookie $http_cookie;
proxy_set_header x-real-ip $http_x_real_ip;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
allow all;
}
}
- php-fpm-nginx环境(Host:local.fatwo.cn && IP:192.168.100.3):一台在IP允许范围内的服务,用于绕过IP限制访问我们想要访问的服务1.fatwo.cn
# 新建文件:/php/demo/index.php 其代码内容如下:
<?php
error_reporting(E_ALL ^ E_NOTICE);
$response = array(
'code' => 200,
'message' => 'ok'
);
header('Content-type:text/json');
$response["data"]["Cookie"] = $_SERVER["HTTP_COOKIE"];
$response["data"]["Host"] = $_SERVER["HTTP_HOST"];
$response["data"]["requestUri"] = $_SERVER["REQUEST_URI"];
$response["data"]["requestMethod"] = $_SERVER["REQUEST_METHOD"];
$response["data"]["userAgent"] = $_SERVER["HTTP_USER_AGENT"];
$response["data"]["user"] = $_SERVER["USER"];
$response["data"]["remoteAddr"] = $_SERVER["REMOTE_ADDR"];
$response["data"]["x-real-ip"] = $_SERVER["HTTP_X_REAL_IP"];
$response["data"]["x-forwarded-for"] = $_SERVER["HTTP_X_FORWARDED_FOR"];
echo json_encode($response);
?>
# 新建文件:/etc/nginx/conf.d/local.fatwo.cn.conf 配置如下:启动解析php服务代码
server {
listen 80;
server_name local.fatwo.cn;
access_log /etc/nginx/logs/local.fatwo.cn.access.log main;
location / {
root /php/demo;
index index.html index.htm index.php;
}
location ~ \.php$ {
root /php/demo;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
# 检查php服务是否可以正确访问
curl -i -H "Host:local.fatwo.cn" http://127.0.0.1/
# 返回状态200说明服务部署成功
开始实验
正常情况下,我们在外网访问1.fatwo.cn(经过nginx层)
curl -H 'Host:1.fatwo.cn' http://172.20.222.65/
- 结果访问提示403 forbidden,因为这个时候IP链为:172.20.222.65,192.168.100.5 (靠近IP限制的nginx1层IP为192.168.100.5不在允许的IP范围内:192.168.100.0/30)
那么我们是否可以绕过nginx1层的IP限制访问服务?
答案是肯定的。这里我们只需要在内网增加一层nginx2层(其IP为192.168.100.2在192.168.100.0/30的允许范围内),即可绕过服务去访问,因为nginx的ip限制仅会匹配就近一层代理的IP。尝试一下访问:
curl -H 'Host:2.fatwo.cn' http://172.20.222.65/
- 结果访问成功,在外网也可以正常访问1.fatwo.cn的服务内容。
- 按照这个实验思路,我们是否可以在公司内网自己做一层代理,去访问一些仅在公司内网才能访问的服务(内网db或者后台管理系统等),避免无法远程处理线上问题的尴尬
- 又或者,我们通过在能访问国外资源的服务器上配置一层代理,也能绕过限制访问到国内无法访问的相关网站资源
关于X-Forwarded-For这个IP链是否可以被伪造?
- 按照其原理,我们不难发现,我们只需要在可以触碰到的一层代理中,将这个值的透传内容修改一下,就可以达到伪造IP的目的。
# 比如,我在修改一下nginx层的配置:将X-Forwarded-For直接设置成固定值,配置如下:
server {
***(此处省略其他配置)
location / {
***(此处省略其他配置)
proxy_set_header X-Forwarded-For "10.10.14.60";
}
}
# 修改完重启一下nginx
nginx -s reload
# 然后在访问一下2.fatwo.cn看看结果:
curl -H 'Host:2.fatwo.cn' http://172.20.222.65/
- 可以看到X-Forwarded-For首层的IP已经被修改成固定的IP:10.10.14.60。
- 按照这个思路,如果外网首层代理IP被伪造(或者错误配置),我们的业务服务器其实就无法正确获取用户的真实IP了。