如何通过Nginx代理识别访问端IP限制对接口的访问 && 如何绕过IP限制访问接口。

3,098 阅读5分钟

业务中有一个通过识别访问端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了。