XFF 要解决的问题
如果客户端直接连接 Web 服务器(真正提供服务的服务器,而不是代理服务器),那么客户端发起的请求中是包含自己的 IP 地址的,这个地址可以通过 remoteAddress 获取到。然而实际情况下并不是这个样子,我们的客户端不可能直接连接真正的服务器,而是会经过一个甚至多个代理服务器访问,例如 Nginx 服务器等。
假设我们的客户端发起的请求经过了三个代理服务器才最终达到真正的服务器:
Client => Proxy1 => Proxy2 => Proxy3 => Web Server
由于每次转发,请求的 remoteAddress 数据会被重新设置为发起请求的服务器地址,因此 Web Server 收到的请求的 remoteAddress 实际上就是 Proxy3 服务器的真实地址。也就是说,Web Server 只知道这个请求是 Proxy3 发来的,至于这个请求是 Proxy3 自己主动发起的,还是它转发的别人的,Web Server 是无力判断的。如果 Web Server 需要获取发起这个请求的原始客户端(Client)的地址,单凭 remoteAddress 肯定做不到。
于是这里就引入了一个 HTTP 扩展头部:X-Forwarded-For (以下简称 XFF) 。
XFF 要解决的问题就是,记录下请求从原始客户端到 Web Server 之间经过的代理服务器的地址。
XFF 是一个事实上的标准 HTTP 头,它不存在于任何既有的标准规范中,但是大家都用。
事实标准:不是由标准阻止制定的,但是业内广泛使用的、大家都认可的标准。
XFF 对应的标准头叫做 Forwarded。
XFF 参数的值
XFF 头的语法格式如下:
X-forwarded-For: IP0, IP1, IP2
其中 IP0 代表的是真实客户端的 IP,IP1 和 IP2 代表的分别是 Proxy1,Proxy2 的 IP 地址。
当 Proxy1 收到 Client 的请求后,它在将这个请求转发给 Proxy2 之前会做两个事情:
- 向 X-Forwarded-For 中添加 Client 的真实地址,可通过 remoteAddress 获取,此时 X-Forwarded-For 的值就是
IP0
- 修改请求的 remoteAddress 为 Proxy1 自己的地址
Proxy2 和 Proxy3 都会重复上述过程,直到 Web Server 收到请求后,此时:
- remoteAddress 为 Proxy3 的真实地址 IP3
- X-forwarded-For 的值为
IP0, IP1, IP2
最后一个代理服务器的真实地址并不用添加到 X-forwarded-For 中,因为这个添加是由它的下家完成的,而 Proxy3 的下家是 Web Server,显然 Web Server 不需要再修改 X-forwarded-For 的数据了。
XFF 的隐患
几个事实:
- remoteAddress 是不能伪造的,因为它是由 TCP 三次握手确定的,伪造会导致建立 TCP 连接失败,请求就不会发生
- X-forwarded-For 是可以伪造的,并且可能植入恶意字符串对服务器进行攻击
这意味着,我们从 X-forwarded-For 头中获取真实客户端的 IP 是不可靠的,它可以被篡改导致与发起请求的机器的实际地址不一致。
如果我们的 Web Server 是通过 Nginx 等服务器转发的,可以一定的配置可以去除这种篡改,例如
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
这条语句表示 Nginx 会将直接与它建立 TCP 连接的客户端或者代理服务器的地址添加到 X-Forwarded-For 中。
如此,Web Server 可以从 X-Forwarded-For 的最后一节中取到直接访问 Nginx 服务器的请求发起者的地址。
这样能保证请求确实是由某个客户端或者代理发起的,而不是伪造的。
在上面的例子中,如果 Proxy3 是一个 nginx 服务器,那么 Web Server 就能保证拿到 Proxy2 的真实地址。
但也仅限于此了,Client 和 Proxy1 的地址虽然可以通过 X-Forwarded-For 获取,但是就不能百分百保证安全了。
安全优先与用户优先
安全优先
如果我们更看重 Web 安全,例如检查用户登录地区,这个时候我们是否能拿到用户的真实地址就不那么重要了,哪怕用户使用的是代理,我们只需要拿到最安全的一个代理地址即可。
此外,直连 Web Server 时 X-Forwarded-For 是可以被篡改的,此时只能通过 remoteAddress 来获取客户端的地址。允许直连会引入新的问题: Web Server 怎么判断请求是直连发起的还是代理发起的?一个思路是通过请求的 remoteAddress 区分:
- 如果请求 Web Server 的代理和 Web Server 运行在同一台机器上,请求的 remoteAddress 为 127.0.0.1,但是这样不能区分请求是直连的,还是从 Web Server 所在服务器上通过代理发起的
- 如果请求 Web Server 的代理和 Web Server 运行在不同的机器上,此时就只能按照预设的 IP 报名单去做判断
为了简洁统一,在使用 Nginx 等服务器代理真实的 Web Server 时,应该禁用 Web Server 的直连。
用户优先
如果我们更加倾向于获取用户的真实地址,例如基于用户的位置做推荐之类的,此时就可以从 X-Forwarded-For 的第一节中获取用户的真实地址,只是这个地址并不是百分百安全的,它可能被篡改过。
由于伪造的 X-Forwarded-For 可以用来攻击,所以在获取用户 IP 前需要做一些判断。
结论
可以直连的服务器,只有 remoteAddress 拿到的地址才是真实的,不要相信任何请求头。
通过 Nginx 等反向代理并且禁用直连的,可以通过 X-Forwarded-For 拿到真实地址。
X-Forwarded-For 第一节代表用户真实地址,但不是百分百安全的。
Web Server 最多只能通过 nginx 等反向代理服务器拿到直连 nginx 服务器的地址,如果经过了多重代理,用户的真实地址是无能为力的,因为代理可能不会提供用户的真实地址。
其它头
X-Real-IP 通常被 HTTP 代理用来表示与它产生 TCP 连接的设备 IP,这个设备可能是其他代理,也可能是真正的请求端。
X-Real-IP 不是一个标准头。
X-Forwarded-Host 记录请求转发过程中经过的服务器的域名。
X-Forwarded-Proto 记录请求转发过程中协议的变化(HTTP/HTTPS)
参考资料
https://imququ.com/post/x-forwarded-for-header-in-http.html
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/X-Forwarded-For
评论区