Laravel中获取真实ip

起因

最近在开发微信支付,微信扫码付以及微信公众号支付对接都是比较顺利,因为 laravel 中 laravel-pay 用起来实在太爽,但是在对接微信的 H5 支付时却怎么也过不了,一直提示「网络环境未能通过安全验证,请稍后再试」。

调查

搜素发现支付常见问题中提到了这个错误,其实简单点就是下单 ip 和支付 ip 不是同一个导致的,只是微信支付只是在 H5 支付环节对 spbill_create_ip 参数进行了校验,而源码中 spbill_create_ip 是通过如下代码获得:

1
2
3
4
5
6
7
8
9
$this->payload = [
'appid' => $config->get('app_id', ''),
'mch_id' => $config->get('mch_id', ''),
'nonce_str' => Str::random(),
'notify_url' => $config->get('notify_url', ''),
'sign' => '',
'trade_type' => '',
'spbill_create_ip' => Request::createFromGlobals()->getClientIp(), // 重点!!!
];

这里是 Request 是来自 Symfony\Component\HttpFoundation\Request ,createFromGlobals 方法恰巧是通过 php 中全局的 $_GET, $_POST,$_COOKIE, $_FILES, $_SERVER 变量构造的,而 $_SERVER 中包含了大量的由 php-fpm 注入的参数。

顺便提一下,laravel 中的 request 也是继承这个 Symfony\Component\HttpFoundation\Request

发现问题

但是通过这个方法无论如何也拿不到真实的用户 ip,由于后台的服务是由 docker 部署,api 服务是通过多个 proxy 代理到最终的 laravel 上,所以在 laravel 上始终获得的 ip 都是其中某个代理的 ip。

翻阅文档,laravel 中有个 TrustProxies 中间件是专门处理 ip 的问题,默认是没有代理是直接读取 REMOTE_ADDR 的头,如果有代理的情况可以填充代理的 ip,但是如果不知道中间代理的 ip 时,可以作如下修改:

1
protected $proxies = '*';

深入问题

可是问题还是没有得到解决,🤣🤣🤣
发现源码中 getClientIp 方法是取得 getClientIps 的第 0 个 ip 地址,那么 getClientIps 方法是可以获取所有的 ip 地址,该方法是从 HTTP_X_FORWARDED_FOR 中拿到所有代理的 ip ,HTTP_X_FORWARDED_FOR 参数是由 nginx 转发时通过 proxy_set_header 添加上去, nginx 的原则是每次在尾部追加代理的ip:

1
HTTP_X_FORWARDED_FOR:真实ip,proxy1,proxy2

BUT getClientIps 返回的 ip 地址数组却变成了:

1
[proxy2,proxy1,真实ip]

所以 getClientIp 每次获取第 0 个参数其实是最后一个代理的 ip。

伪解决问题

我在php的入口函数 index.php 的顶部加入了如下一段:

1
2
3
4
// 这是一个很奇怪很奇怪的问题,可能是 laravel5.7  的bug,也可能是 TrustProxies 的 bug ,获取到的IP顺序是反的
if ($_SERVER['HTTP_X_FORWARDED_FOR']) {
$_SERVER['HTTP_X_FORWARDED_FOR'] = implode(',', array_reverse(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])));
}

在程序执行到这里的时候我将 HTTP_X_FORWARDED_FOR 中的 ip 进行倒序,getClientIp 拿到的第 0 个 ip 即为真实 ip 。

虽然没有完美解决这个问题,但在使用中也没有发现其他 bug ,如果你有更多发现,欢迎联系我:`jake.zou.me@gmail.com`

坚持原创技术分享,您的支持将鼓励我继续创作!