记一次解决反向代理后Laravel中获取不到真实IP问题

前言

因为现在很流行前后端分离,这样可以更好地管理前后端代码以及前后端分工。不过前后端分离也不是万能的,前后端分离也带来了一些问题,其中跨域访问问题就是很常见的一个。通过设置相应头可以解决跨域问题,可是同时每次请求后端接口的时候,前端都需要多发送一个 OPTIONS 请求咨询自己是否可以请求,请求太多看起来十分影响颜值。于是我们选择了使用反向代理后端的 API 使前后端都在同一个域下进而规避掉跨域的问题。

一开始使用自然是相当美好的,这个跟恋爱刚开始一样。然而上周有同事做一个需要收集客户IP的需求时才发现了后端获取到的客户IP并不正确!

检查PHP代码

同事是通过 Laravel框架的app(\Illuminate\Http\Request::class)->ip()来获取客户端的IP的,找到它:

ip()

找到方法定义发现是调用了getClientIp()这个方法来获取客户端IP的,然而Ctrl+F大法并没有在当前类找到该方法,于是向上翻找,发现这个类是继承了Symfony\Component\HttpFoundation\Request类,OK,继续往上找:

getClientIp()

终于找到了getClientIp()这个方法,结果发现居然是通过getClientIps()这个方法获得的,好吧,成功就在眼前,让我们继续找到getClientIps()

getClientIps()

出现了!原来是通过$_SERVER['REMOTE_ADDR']这个变量来获取客户端IP的!然后判断是不是来自可信任的代理,因为静态变量$trustedProxies默认情况下没有设置,所以isFromTrustedProxy()方法返回的值是false,所以直接返回了$_SERVER['REMOTE_ADDR']获取到的值。

接下来我们尝试打印Illuminate\Http\Request::ip()方法获取到的IP以及$_SERVER['REMOTE_ADDR']。代码如下:

1
2
dump('app(Illuminate\Http\Request::class)->ip(): ' . app(Illuminate\Http\Request::class)->ip());
dump('$_SERVER["REMOTE_ADDR"]' . $_SERVER['REMOTE_ADDR']);

输出结果如下:

ip-output

果不其然,如我们所料的一样!经查询发现获得的IP就是我们项目所在的服务器!因为代理导致REMOTE_ADDR没有能正确获取到客户端那边的IP。

解决方案(Apache)

原因找到之后,相应的解决方案也就呼之欲出。因为我们用的是Apache,所以首先给出Apache的解决方案。

Apache需要启用一个remoteip的模块,然后在api的虚拟主机配置文件中加上一行代码即可,如下:

1
2
3
4
<VirtualHost _default_:443>
# ...
RemoteIPHeader X-Forwarded-For
</VirtualHost>

加好之后,我们reload apache看看效果:

real-ip

Awesome!果然显示的是真实的客户IP了!

解决方案(Nginx)

后续有其他同事反馈说另一个项目也出现了同样的问题,我一看,阿咧,Nginx,好吧,难不倒我的。经过一番搜索,终于找到了解决方案。

首先我们的前端虚拟主机配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
location /api {
proxy_pass http://your-api-domain.com;

#proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header HTTP_X_FORWARDED_FOR $remote_addr;
proxy_redirect default;
}

然后在API虚拟主机配置文件中加入下面一行:

1
2
3
4
server {
# ...
real_ip_header X-Forwarded-For;
}

然后reload Nginx即可。

解决方案(Laravel)

其实直接通过设置上文所说的$trustedProxies静态变量即可正确获取客户端的真实IP。由于时间关系,我仅测试了Laravel 5.8版本,使用下面代码即可正确获取客户端真实IP:

1
2
request()->setTrustedProxies(request()->getClientIps, \Illuminate\Http\Request::HEADER_X_FORWARDED_FOR);
dump(request()->ip());

为了方便,可以新建一个中间件然后将上面设置trustedProxies静态变量的那行代码加进去,然后就可以完美解决无法获取到客户端真实IP的问题啦!

Happy Ending

本篇文章到这就全部结束了,感谢大家的阅读。希望大家能从这篇文章中得到帮助。如果你对这篇文章有任何意见或建议,都欢迎给我发送邮件,优质的文章离不开读者的反馈。