PHP (Laravel) 实现 iOS 内购服务端验证
IOS内购的流程大致如下:
- 通过产品ID获取产品信息列表(客户端的事,我们不用管)
- 添加监听(客户端的事,我们不用管)
- 把产品包装成SKPayment(支付)发送给苹果服务器(客户端的事,我们不用管)
- 苹果服务器购买成功后会回调监听方法,根据苹果服务器返回信息判断是否购买成功。
- 购买失败或已经购买过该商品则注销交易。如果购买成功,此时可以向自家服务器发送购买成功的消息,并通过后台向苹果服务器发送验证,然后注销交易。(这一步,客户端会得到苹果返回的支付凭证数据,然后用此数据发到我们服务端,服务端进行验证)
代码如下
/**
* @Author woann <www.woann.cn>
* @param Request $request
* @return \Illuminate\Http\JsonResponse
* @des ios内购支付回调
*/
public function iosNotify(Request $request)
{
$this->validate(
$request,
[
'order_id' => 'required|digits_between:1,9|integer',
'apple_receipt' => 'required|min:20',
]
);
//苹果内购的验证收据
$receipt_data = $request->input('apple_receipt');
$order_id = $request->input('order_id');
$order = LevelOrder::find($order_id);
if (!$order || $order->state != 0) {
return returnApi(500,'订单状态异常');
}
$ios_sandBox = env('IOS_SANDBOX',true);//判断生产环境,开发环境
if(empty($receipt_data)){
return json(500, '参数不正确');
}
// 获取校验结果
$result=$this->validate_apple_pay($receipt_data,$ios_sandBox);
if (!$result || !is_array($result) || !isset($result['status'])) {
return returnApi(500,'获取数据失败,请重试');
}
//如果校验失败
if ($result['status'] != 0) {
return returnApi(500,'miss [apple_receipt]');
}
if ($result['status'] == 0) {
$transaction_ids = [];//用来收集需要关闭的transaction_id
//遍历未结束交易的列表(in_app)
foreach ($result['receipt']['in_app'] as $v){
$transaction_ids[] = $v['transaction_id'];//不管当前交易是否已经处理过,都要返回给客户端结束交易
$is_exist = DB::table('ios_transaction_id')->where('transaction_id', $v['transaction_id'])->first();
if ($is_exist) {
//如果已经处理过该订单 直接跳过
continue;
}
//否则在此处理业务逻辑
//...
//然后记录下此transaction_id标记为已处理过
DB::table('ios_transaction_id')->insert(['transaction_id' => $v['transaction_id']]);
}
//返回给客户端需要结束交易的transaction_id列表
return returnApi(200,'SUCCESS',['transaction_ids' => $transaction_ids]);
}
}
/**
* 验证AppStore内付
* @param string $receipt_data 付款后凭证
* @return array 验证是否成功
*/
protected function validate_apple_pay($receipt_data,$ios_sandBox)
{
/**
* 21000 App Store不能读取你提供的JSON对象
* 21002 receipt-data域的数据有问题
* 21003 receipt无法通过验证
* 21004 提供的shared secret不匹配你账号中的shared secret
* 21005 receipt服务器当前不可用
* 21006 receipt合法,但是订阅已过期。服务器接收到这个状态码时,receipt数据仍然会解码并一起发送
* 21007 receipt是Sandbox receipt,但却发送至生产系统的验证服务
* 21008 receipt是生产receipt,但却发送至Sandbox环境的验证服务
*/
$POSTFIELDS = '{"receipt-data":"'. $receipt_data .'"}';
if($ios_sandBox){
// 请求验证
$data = httpRequest('https://sandbox.itunes.apple.com/verifyReceipt', $POSTFIELDS);
}else{
// 请求验证
$data = httpRequest('https://buy.itunes.apple.com/verifyReceipt', $POSTFIELDS);
}
return $data;
}
protected function httpRequest($url, $postData = array(), $json = true)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
if ($postData) {
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
}
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
$info = curl_getinfo($ch);
$data = curl_exec($ch);
curl_close($ch);
if ($json) {
return json_decode($data, true);
} else {
return $data;
}
}
```
本作品采用《CC 协议》,转载必须注明作者和本文链接
这么简单的判断是认真的吗 :joy:
@woann 最简单的,人家伪造支付凭证,上传一个支付了一分钱的凭证,又或者上传2018年支付的支付凭证,不就GG了吗?破解前端的接口是分分钟的事情
确实,苹果票据是可以伪造的,或者说劫持代充等
@伍同学 修改了,看了下相关资料,确实需要增加校验拦截,增加了对transaction_id重复处理的判断(插入数据库,回调前先判断是否存在),并增加了给客户端返回transaction_id,让客户端结束对应交易,不然交易列表一直累计。
@wind 修改了,看了下相关资料,确实需要增加校验拦截,增加了对transaction_id重复处理的判断(插入数据库,回调前先判断是否存在),并增加了给客户端返回transaction_id,让客户端结束对应交易,不然交易列表一直累计。
苹果服务器调用的接口处理可以分享一下吗
可以简化很多的 写过相同的功能
@Rekkles 我之前就是差不多这样,这样是不行的,你只校验status的值,而不校验in_app里的交易列表是否已经处理过,会被利用这点重复刷单。
@woann receipt-data 是黑名单处理的 而且你接口算上加密和签名 基本上没啥重放攻击的可能性 而一般来说内购在上线之后都是关闭状态 :joy:
ios内购支付成功后,ios不会异步回调来通知我们服务器吗?只能拿客户端返回的凭证来检测是否支付成功吗?
@Potato1 是的,跟国内的微信和支付宝不一样,很恶心的。
意思一客户端请求一次要处理所有的订单么
in_app 里面是多条信息 只有一个product_id 只知道是哪一个产品 不知道对应的是哪一个订单呀?怎么解决呀
苹果是不是会出现验证时返回空,然后在多验证几次就可以拿到数据了
分分钟被刷单