起因
最近在开发微信支付,微信扫码付以及微信公众号支付对接都是比较顺利,因为 laravel 中 laravel-pay 用起来实在太爽,但是在对接微信的 H5 支付时却怎么也过不了,一直提示「网络环境未能通过安全验证,请稍后再试」。
调查
搜素发现支付常见问题中提到了这个错误,其实简单点就是下单 ip 和支付 ip 不是同一个导致的,只是微信支付只是在 H5 支付环节对 spbill_create_ip 参数进行了校验,而源码中 spbill_create_ip 是通过如下代码获得:
$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 时,可以作如下修改:
protected $proxies = '*';
深入问题
可是问题还是没有得到解决,🤣🤣🤣
发现源码中 getClientIp 方法是取得 getClientIps 的第 0 个 ip 地址,那么 getClientIps 方法是可以获取所有的 ip 地址,该方法是从 HTTP_X_FORWARDED_FOR
中拿到所有代理的 ip ,HTTP_X_FORWARDED_FOR
参数是由 nginx 转发时通过 proxy_set_header
添加上去, nginx 的原则是每次在尾部追加代理的ip:
HTTP_X_FORWARDED_FOR:真实ip,proxy1,proxy2
BUT getClientIps 返回的 ip 地址数组却变成了:
[proxy2,proxy1,真实ip]
所以 getClientIp 每次获取第 0 个参数其实是最后一个代理的 ip。
伪解决问题
我在php的入口函数 index.php 的顶部加入了如下一段:
// 这是一个很奇怪很奇怪的问题,可能是 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