nginx能够处理重复的X-Forwarded-For头吗?

4

当用户使用代理(例如Google数据节省器)时,浏览器会为客户端的真实IP地址添加X-Forwarded-For。我们的负载均衡器将所有标头+客户端IP地址作为X-Forwarded-For标头传递到Nginx服务器。以下是示例请求标头:

X-Forwarded-For: 1.2.3.4
X-Forwarded-Port: 80
X-Forwarded-Proto: http
Host: *.*.*.*
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8,tr;q=0.6
Save-Data: on
Scheme: http
Via: 1.1 Chrome-Compression-Proxy
X-Forwarded-For: 1.2.3.5
Connection: Keep-alive

有没有办法将两个X-Forwarded-For头分别传递给php?


如果我没记错的话,第二个会覆盖第一个。你的负载均衡器应该执行 X-Forwarded-For: 1.2.3.4,1.2.3.5 - Charlotte Dunois
无法更改,因为它是托管服务。 - onesvat
你的 PHP 脚本目前返回了什么? - Tarun Lalwani
nginx肯定可以处理这个问题,但是不清楚你所说的设置是什么样的。 - Narf
4个回答

7

TL;DR

  • nginx: fastcgi_param HTTP_MERGED_X_FORWARDED_FOR $http_x_forwarded_for
  • php: $_SERVER['HTTP_MERGED_X_FORWARDED_FOR']

解释

你可以使用$http_<header_name>变量来访问所有的HTTP头。当使用此变量时,nginx甚至会为您执行头部合并,所以您不需要自己进行处理。

CustomHeader: foo
CustomHeader: bar

被翻译为值:

foo, bar

因此,您所需做的就是将此变量传递给php 使用fastcgi_param
fastcgi_param HTTP_MERGED_X_FORWARDED_FOR $http_x_forwarded_for

概念证明:

在您的nginx服务器块中:

location ~ \.php$ {
    fastcgi_pass unix:run/php/php5.6-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param HTTP_MERGED_X_FORWARDED_FOR $http_x_forwarded_for;
    include fastcgi_params;
}

test.php

<?php
die($_SERVER['HTTP_MERGED_X_FORWARDED_FOR']);

最后,看看使用curl会发生什么:

curl -v -H 'X-Forwarded-For: 127.0.0.1' -H 'X-Forwarded-For: 8.8.8.8' http://localhost/test.php

给出以下响应:
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 80 (#0)
> GET /test.php HTTP/1.1
> Host: localhost
> User-Agent: curl/7.47.0
> X-Forwarded-For: 127.0.0.1
> X-Forwarded-For: 8.8.8.8
> 
< HTTP/1.1 200 OK
< Server: nginx/1.10.3 (Ubuntu)
< Date: Wed, 01 Nov 2017 09:07:51 GMT
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< 
* Connection #0 to host localhost left intact
127.0.0.1, 8.8.8.8

瞧!这里有一个逗号分隔的字符串,包含了所有X-FORWARDED-FOR头信息,可以通过$_SERVER['HTTP_MERGED_X_FORWARDED_FOR']访问。

当然,你可以使用任何你想要的名称,而不仅仅是HTTP_MERGED_X_FORWARDED_FOR


不错!你知道nginx合并头部吗?是在哪里找到的(哪里?)还是自己发现的? - MatTheCat
据我所知,虽然它没有正式记录,但该行为受RFC支持,并且已知add_header存在相同的行为,此外,在错误跟踪器维基中也暗示这是首选的行为(这与$proxy_add_x_forwarded_for相反,后者可以正确地连接多个传入标头)。 - Xyz
嗯,是的,我猜我自己在某个地方找到了答案。虽然,我有点直觉。 - Xyz

1

您可以在变量$realip_remote_addr中获取连接ELB的原始客户端地址,但请注意,此变量仅在nginx 1.9.7中添加,因此您需要运行非常新的nginx版本。

有关更多信息,请参见ngx_http_realip_module variables

例如,使用此配置:

set_real_ip_from 127.0.0.1;
set_real_ip_from 192.168.2.1;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

并且有一个 X-Forwarded-For 头部产生:

X-Forwarded-For: 123.123.123.123, 192.168.2.1, 127.0.0.1

默认情况下,nginx会选择最左侧的IP地址123.123.123.123作为客户端的IP地址,除了受信任的代理。
但是,$realip_remote_addr保留原始客户端地址。

如果有多个,您能确认它是否合并了“real_ip_header”吗? - MatTheCat
@MatTheCat 无法确认。 - Suraj

1
你需要处理的问题需要在Web服务器层面解决。因此,我创建了两个服务器,一个使用Apache,另一个使用Nginx来解决这个问题。测试命令:
curl -H "X: Y" -H "X: Z"  http://localhost:8088/router.php | jq

Apache

使用 Apache 执行时,输出结果如下:

{
  "HEADERS": {
    "Host": "localhost:8088",
    "User-Agent": "curl/7.47.0",
    "Accept": "*/*",
    "X": "Y, Z"
  }
}

正如您所看到的,我们向apache传递了两个头文件,apache使用,将它们组合在一起。如果我们改变第一个头文件已经包含,,它仍然可以正常工作。
$ curl -H "X: Y, A" -H "X: Z"  http://localhost:8088/router.php | jq
{
  "HEADERS": {
    "Host": "localhost:8088",
    "User-Agent": "curl/7.47.0",
    "Accept": "*/*",
    "X": "Y, A, Z"
  }
}

Nginx

现在在 Nginx 上发送同样的请求会产生以下结果:

{
  "HEADERS": {
    "X": "Z",
    "Accept": "*/*",
    "User-Agent": "curl/7.47.0",
    "Host": "localhost"
  }
}

现在,Nginx确实会将这些标头发送到PHP-FPM,但PHP-FPM不会将这些重复的标头合并为一个。因此,在脚本中,您只会得到最新的标头。
编辑-1:使用fastcgi_param合并
感谢@AronCederholm指出,通过指定FASTCGI_PARAM可以实现合并。
我最初尝试了相同的方法,但结果是空白标头。我曾尝试添加
fastcgi_param X-Forwarded-For $http_x_forwarder_for;

刚才在阅读他的消息后,我意识到我的配置文件中有一个错别字。应该是


fastcgi_param X-Forwarded-For $http_x_forwarded_for;

在这个更改之后,标题可以正常工作。但是它不会出现在getallheaders()中。如下响应所示,它将通过$_SERVER[]可用。

$ curl -v -H 'X-Forwarded-For: 127.0.0.1' -H 'X-Forwarded-For: 8.8.8.8' http://localhost/router.php | jq
{
  "HEADERS": {
    "X-Forwarded-For": "8.8.8.8",
    "Accept": "*/*",
    "User-Agent": "curl/7.47.0",
    "Host": "localhost"
  },
  "SERVER": {
    "USER": "vagrant",
    "HOME": "/home/vagrant",
    "HTTP_X_FORWARDED_FOR": "8.8.8.8",
    "HTTP_ACCEPT": "*/*",
    "HTTP_USER_AGENT": "curl/7.47.0",
    "HTTP_HOST": "localhost",
    "X-Forwarded-For": "127.0.0.1, 8.8.8.8",

原始答案

很不幸,我没有找到任何适用于Nginx或PHP-FPM的设置或插件,可以将重复的标头合并为一个。而且你无法在PHP级别处理这种情况,因为你永远无法看到原始标头。

可能的解决方案

  1. 将Apache放在Nginx前面。让nginx监听unix套接字,并使用apache反向代理请求到nginx
  2. 用Apache替换Nginx
  3. 创建一个Nginx插件来合并标头。以下两个项目应该能帮助你入门

https://github.com/giom/nginx_accept_language_module

https://github.com/openresty/headers-more-nginx-module


实际上,nginx 会为您执行头部合并。请参阅我的答案,了解如何触发此功能。 - Xyz
@AronCederholm,感谢您的指出。我再次深入研究后意识到了我的错误所在。在测试相同解决方案时,我打错了一个字母,以为nginx没有处理好这种情况,而没有仔细检查我的配置。 - Tarun Lalwani

0

X-Forwarded-For的头信息应该由请求中每个代理内联附加。您不应该收到两个标题。因为按设计追加了值,所以任何人都可以向该列表添加IP地址,因此不要将其用于安全检查。如果您需要检查IP地址以确保安全性,请在Web服务器上设置X-Real-IP头,覆盖任何传入的值。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接