支付宝PC扫码沙箱支付流程,使用新版的RSA2支付
框架我使用了ThinkPHP5
1.首先登陆蚂蚁金服https://openhome.alipay.com/developmentDocument.htm
登陆之后 点开头像 点击账户管理(需要申请沙箱环境,此处省略)
2.点击开发中心-研发服务
3.获取支付参数 APPID 支付宝网关 支付宝公钥和商户私钥
第一次没有支付宝公钥的,我先要用支付宝的生成工具生成一个。
生成方法:
3.1.下载生成工具(https://docs.open.alipay.com/291/105971/)
3.2.解压之后双击运行 RSA签名验签工具.bat
3.3.因为我是PHP程序,所以勾了非JAVA使用,为了提高安全,密钥长度我用了2048的
3.4复制生成的商户应用公钥,到支付宝后台 点击 RSA2(SHA256)密钥(推荐) 把复制的应用公钥粘贴进去,点击保存
3.5.保存之后点击 [查看支付宝公钥],把支付宝公钥复制下来,把商户应用私钥(3.3图)和支付宝公钥 写进程序的配置文件
4.熟悉一下支付流程 https://docs.open.alipay.com/270/105898
5. 查看支付接口 https://docs.open.alipay.com/api_1/alipay.trade.page.pay/
6.按照支付接口开发文档编写请求参数
如:
$alipayConfigs = config('alipay');
$params = [
'app_id' => $alipayConfigs['APPID'],
'method' => 'alipay.trade.page.pay', //接口名称 固定值alipay.trade.page.pay
'format' => 'JSON', //目前仅支持JSON
'return_url' => $alipayConfigs['return_url'], //同步返回地址
'charset' => 'UTF-8',
'sign_type' => 'RSA2', //签名方式
'sign' => '', //签名
'timestamp' => date('Y-m-d h:i:s'), //发送时间
'version' => '1.0', //固定1.0
'notify_url' => $alipayConfigs['notify_url'], //异步通知地址
'biz_content' => '', //业务请求参数的集合
];
$defaultParam = [
'out_trade_no' => $orderInfo['orderid'], //商户订单号
'product_code' => 'FAST_INSTANT_TRADE_PAY', //销售产品码.固定值
'total_amount' => 0.01, //总价 单位为元
'subject' => '罗小罗商城', //订单标题
];
$params['biz_content'] = json_encode($defaultParam,JSON_UNESCAPED_UNICODE);
值得注意的是文档中的 biz_content参数,官方说的是请求参数的集合,除公共参数外所有请求参数都必须放在这个参数中传递。
于是我就把defaultParam参数数组转json之后放到biz_content上。
7.获得参数params的签名
签名步骤参考官方文档(https://docs.open.alipay.com/291/106118):
7.1筛选并排序
获取所有请求参数,不包括字节类型参数,如文件、字节流,剔除sign字段,剔除值为空的参数,并按照第一个字符的键值ASCII码递增排序(字母升序排序),如果遇到相同字符则按照第二个字符的键值ASCII码递增排序,以此类推。
unset($params['sign']); //剔除sign
ksort($params); //进行排序
7.2拼接
将排序后的参数与其对应值,组合成“参数=参数值”的格式,并且把这些参数用&字符连接起来,此时生成的字符串为待签名字符串。
再进行urldecode
$queryUrl = urldecode(http_build_query($params));
7.3此时生成的字符串为待签名字符串,然后调用签名函数
使用各自语言对应的SHA256WithRSA(对应sign_type为RSA2)或SHA1WithRSA(对应sign_type为RSA)签名函数利用商户私钥对待签名字符串进行签名,并进行Base64编码
$sign = createSign($queryUrl);
//createSign方法声明
function createSign($data = '')
{
if (!is_string($data)) {
return null;
}
$res = getPrivateKey();
$sign = openssl_sign($data, $sign, $res,OPENSSL_ALGO_SHA256 ) ? base64_encode($sign) : null;
return $sign;
}
//getPrivateKey方法声明
function getPrivateKey()
{
$alipayConfigs = config('alipay');
$privKey = $alipayConfigs['APPPRI_KEY']; //这个是商户私钥
$search = [
"-----BEGIN RSA PRIVATE KEY-----",
"-----END RSA PRIVATE KEY-----",
"\n",
"\r",
"\r\n"
];
$privKey = str_replace($search,"",$privKey);
$private_key = $search[0] . PHP_EOL . wordwrap($privKey, 64, "\n", true) . PHP_EOL . $search[1];
$res = openssl_pkey_get_private($private_key);
return $res;
}
得到签名之后,要把得到的签名加入到params数组中
$params['sign'] = $sign;
8.获得一个带签名的params请求参数之后,构造一个请求URL
//alipayConfigs['GATEWAG'] 是支付网关 沙箱调试的网关
//是:https://openapi.alipaydev.com/gateway.do
$url = $alipayConfigs['GATEWAG']. http_build_query($params);
9.访问这个URL就可以发起支付
部分代码:
public function addOrderByWangye($orderInfo)
{
$alipayConfigs = config('alipay');
$params = [
'app_id' => $alipayConfigs['APPID'],
'method' => 'alipay.trade.page.pay', //接口名称 固定值alipay.trade.page.pay
'format' => 'JSON', //目前仅支持JSON
'return_url' => $alipayConfigs['return_url'], //同步返回地址
'charset' => 'UTF-8',
'sign_type' => 'RSA2', //签名方式
'sign' => '', //签名
'timestamp' => date('Y-m-d h:i:s'), //发送时间
'version' => '1.0', //固定1.0
'notify_url' => $alipayConfigs['notify_url'], //异步通知地址
'biz_content' => '', //业务请求参数的集合
];
$defaultParam = [
'out_trade_no' => $orderInfo['orderid'], //商户订单号
'product_code' => 'FAST_INSTANT_TRADE_PAY', //销售产品码.固定值
'total_amount' => 0.01, //总价 单位为元
'subject' => '罗小罗商城', //订单标题
];
$params['biz_content'] = json_encode($defaultParam,JSON_UNESCAPED_UNICODE);
$params = setSign($params);
//和微信支付不同 支付宝生成的链接不用进行URL编码 直接传输即可
//alipayConfigs['GATEWAG'] 是支付网关 沙箱调试的网关
//是:https://openapi.alipaydev.com/gateway.do
$url = $alipayConfigs['GATEWAG']. http_build_query($params);
echo json_encode([
'url'=>$url
]);
exit;
}
//返回一个带签名的数组
function setSign($params)
{
unset($params['sign']);
ksort($params);
$queryUrl = urldecode(http_build_query($params));
$sign = createSign($queryUrl);
$params['sign'] = $sign;
return $params;
}
//生成签名
function createSign($data = '')
{
if (!is_string($data)) {
return null;
}
$res = getPrivateKey();
$sign = openssl_sign($data, $sign, $res,OPENSSL_ALGO_SHA256 ) ? base64_encode($sign) : null;
return $sign;
}
//获取私钥
function getPrivateKey()
{
$alipayConfigs = config('alipay');
$privKey = $alipayConfigs['APPPRI_KEY']; //商户私钥
$search = [
"-----BEGIN RSA PRIVATE KEY-----",
"-----END RSA PRIVATE KEY-----",
"\n",
"\r",
"\r\n"
];
$privKey = str_replace($search,"",$privKey);
$private_key = $search[0] . PHP_EOL . wordwrap($privKey, 64, "\n", true) . PHP_EOL . $search[1];
$res = openssl_pkey_get_private($private_key);
return $res;
}
10.支付成功操作
10.1.用户确认支付后,支付宝get请求returnUrl(商户入参传入),返回同步返回参数。
10.2.交易成功后,支付宝post请求notifyUrl(商户入参传入),返回异步通知参数。
值得注意的是:
由于同步返回的不可靠性,支付结果必须以异步通知或查询接口返回为准,不能依赖同步跳转.
11.异步通知处理
11.1获取异步通知参数
$postData = Request()->post();
11.2获得异步通知参数里的签名
$sign = $postData['sign'];
11.3进行验签(签名之前需要删除签名和签名类型这个要十分注意,如果不删除会导致签名不一样)
unset($postData['sign']);
unset($postData['sign_type']);
11.4对异步通知参数进行排序
ksort($postData);
11.5.得到验签字符串
$str = urldecode(http_build_query($postData));
11.6调用验签算法
/******RSA2验签算法****** */
/*
data 需要验签的字符串
public_key 支付宝公钥
sign 异步通知支付宝的签名
返回 bool
true : 验签失败
false : 验签成功
*/
function rsaCheck($data, $public_key, $sign,$type = 'RSA2')
{
$search = [
"-----BEGIN PUBLIC KEY-----",
"-----END PUBLIC KEY-----",
"\n",
"\r",
"\r\n"
];
$public_key = str_replace($search,"",$public_key);
$public_key = $search[0] . PHP_EOL . wordwrap($public_key, 64, "\n", true) . PHP_EOL . $search[1];
$res = openssl_get_publickey($public_key);
if($res)
{
if($type == 'RSA'){
$result = (bool)openssl_verify($data, base64_decode($sign), $res);
}elseif($type == 'RSA2'){
$result = (bool)openssl_verify($data, base64_decode($sign), $res,OPENSSL_ALGO_SHA256);
}
openssl_free_key($res);
}else{
exit("公钥格式有误!");
}
return $result;
}
11.7验证签名通过之后,验证是否是支付宝发来的通知(https://docs.open.alipay.com/58/103597),这个步骤我觉得还是很有必要验证的,万一是恶意用户发来的假通知呢?
获取支付宝通知回来的参数notify_id
$notify_id = $postData['notify_id'];
请求示例:
https://mapi.alipay.com/gateway.do?service=notify_verify&partner=2088002396712354¬ify_id=RqPnCoPT3K9%252Fvwbh3I%252BFioE227%252BPfNMl8jwyZqMIiXQWxhOCmQ5MQO%252FWd93rvCB%252BaiGg
为了方便我使用file_get_contents请求请求成功后得到,线上的话建议使用curl get。如果以上url请求成功,则
处理结果有两种:
a.成功时:true
b.不成功时:报对应错误
需要注意的是,当通知是支付宝发来的通知,请求返回的true是字符串类型的true,并不是布尔型的true
/*******判断支付通知是否真的来自支付宝********* */
//check_url:https://mapi.alipay.com/gateway.do?
//service=notify_verify&partner=¬ify_id=
public function isAlipayNotiy($notify_id)
{
$checkUrl = config('alipay.check_url') . $notify_id;
$res = file_get_contents($checkUrl);
Db::name('error')->insert(['error'=>$checkUrl]);
return $res == 'true';
}
11.8 验签和通知来源合法之后,验证订单状态和金额 判断交易状态(https://docs.open.alipay.com/270/105902/)
以下代码,只为说明
if($postData['trade_status'] == 'TRADE_SUCCESS' || $postData['trade_status'] == 'TRADE_FINISHED')
{
//用户确实已经支付成功
//取出支付宝订单号 拿着这个订单号去查我的内部订单
$aliOrderid = $postData['out_trade_no'];
//内部订单结果
$pay = Db::name('Order')->where('orderid',$aliOrderid)->value('pay');
//假设 pay=1 为支付成功 0 是用户未支付
if($pay || $pay == null){
//输出success 告诉支付宝 这个订单我已经处理过了 或 这个订单在我的数据库里面找不到 不要再
//通知我了
echo 'success';
exit;
}
//处理内部逻辑
$upData['paytime'] = date('Y-m-d H:i:s');
$upData['pay'] = 1;
if(Db::name('Order')->where('orderid',$aliOrderid)->update($upData)){
//输出success 告诉支付宝 这个订单我处理好了 不要再通知我了
echo 'success';
exit;
}
}
注意事项:
- 必须保证服务器异步通知页面(notify_url)上无任何字符,如空格、HTML标签、开发系统自带抛出的异常提示信息等;
- 支付宝是用POST方式发送通知信息,因此该页面中获取参数的方式,如:request.Form(“out_trade_no”)、$_POST[‘out_trade_no’];
- 支付宝主动发起通知,该方式才会被启用;
- 只有在支付宝的交易管理中存在该笔交易,且发生了交易状态的改变,支付宝才会通过该方式发起服务器通知(即时到账交易状态为“等待买家付款”的状态默认是不会发送通知的);
- 服务器间的交互,不像页面跳转同步通知可以在页面上显示出来,这种交互方式是不可见的;
- 第一次交易状态改变(即时到账中此时交易状态是交易完成)时,不仅会返回同步处理结果,而且服务器异步通知页面也会收到支付宝发来的处理结果通知;
- 程序执行完后必须打印输出“success”(不包含引号)。如果商户反馈给支付宝的字符不是success这7个字符,支付宝服务器会不断重发通知,直到超过24小时22分钟。一般情况下,25小时以内完成8次通知(通知的间隔频率一般是:4m,10m,10m,1h,2h,6h,15h);
- 程序执行完成后,该页面不能执行页面跳转。如果执行页面跳转,支付宝会收不到success字符,会被支付宝服务器判定为该页面程序运行出现异常,而重发处理结果通知;
- cookies、session等在此页面会失效,即无法获取这些数据;
- 该方式的调试与运行必须在服务器上,即互联网上能访问;
- 该方式的作用主要防止订单丢失,即页面跳转同步通知没有处理订单更新,它则去处理;
- 当商户收到服务器异步通知并打印出success时,服务器异步通知参数notify_id才会失效。也就是说在支付宝发送同一条异步通知时(包含商户并未成功打印出success导致支付宝重发数次通知),服务器异步通知参数notify_id是不变的。
个人博客:https://www.521bug.cn