支付开发填坑记之支付宝

支付宝在所有支付方式中最好开发的了,因为文档比较清晰,而且开发起来也比较简单。因此,支付宝的坑是相对较少的。
原文地址

APP支付

APP支付步骤为:

  1. 获取支付宝的配置信息。
  2. 生成商家订单信息。
  3. 根据订单信息生成待校验数据
  4. 生成请求给支付宝的加密字符串
  5. 将待校验数据和加密字符串拼接,返回给APP。
  6. APP将得到的数据请求支付宝客户端进行支付。

由于APP支付是由APP去调起支付宝支付,所以服务端需要做的事情就是将请求参数封装好之后返回APP即可。

  1. 获取支付宝的配置信息。
    支付时需要的配置信息有:

    • key: 交易安全校验码。
    • app_id:支付宝分配给开发者的应用ID。
  2. 生成商家订单信息。
    这个步骤由商家自行生成。支付宝那边只需要知道的订单信息为:

    • subject: 必填。商品的标题/交易标题/订单标题/订单关键字等。
    • total_amount: 必填。订单价格。
    • out_trade_no: 必填。商户网站唯一订单号。
    • body: 非必填。交易的具体描述信息。
  3. 根据订单信息生成待校验数据
    APP支付的详细请求参数: 点击查看
    APP-支付宝
  4. 生成请求给支付宝的加密字符串

    $sign = $alipaySubmit->buildRequestParaForApp($para_token);

    其中, buildRequestParaForApp 的实现为:

    1. 对待签名参数数组排序
    /**
     * 对数组排序
     * @param $para 排序前的数组
     * return 排序后的数组
     */
    function argSort($para) {
        ksort($para);
        reset($para);
        return $para;
    }
    1. 生成签名结果(阿里推荐的是RSA2的签名方式,这里项目用的是RSA)
    /**
     * RSA签名
     * @param $data 待签名数据
     * @param $private_key_path 商户私钥文件路径
     * return 签名结果
     */
    function rsaSign($data, $private_key_path) {
        $priKey = file_get_contents($private_key_path);
        $res = openssl_get_privatekey($priKey);
        openssl_sign($data, $sign, $res);
        openssl_free_key($res);
        //base64编码
        $sign = base64_encode($sign);
        return $sign;
    }
  5. 将待校验数据和加密字符串拼接,返回给APP。

    $url = "";
    foreach ($para_token as $key => $value) {
        $url .= $key."=".urlencode($value)."&";
    }
    return $url."sign=".urlencode($sign);
  6. APP将得到的数据请求支付宝客户端进行支付。
    APP端将拼接好的字符串拿去请求支付宝客户端即可调起支付宝进行支付。拼接好的字符串大致如下图所示:
    APP-支付宝-APP

网页版支付

网页版支付步骤为:

  1. 设置支付宝的配置信息。
  2. 向支付宝申请新订单,获取支付token。
  3. 携带token进行订单支付。

网页版的支付宝支付相对于APP调起支付宝要复杂,因为网页支付时,需要多次请求支付宝服务器获取支付的必要参数。

  1. 设置支付宝配置信息。

    /**调用授权接口alipay.wap.trade.create.direct获取授权码token**/
            
        //返回格式
        private  $format = "";
        //必填,不需要修改
        
        //版本
        private $v = "";
        //必填,不需要修改
        
        //请求号
        private $req_id = "";
        //必填,须保证每次请求都是唯一
        
        //**req_data详细信息**
        
        //服务器异步通知页面路径
        private $notify_url = "";
        //需http://格式的完整路径,不允许加?id=123这类自定义参数
        
        //页面跳转同步通知页面路径
        private $call_back_url = "";
        //需http://格式的完整路径,不允许加?id=123这类自定义参数
        
        //卖家支付宝账户
        private $seller_email = "";
        //必填
        
        //商户订单号
        private $out_trade_no = "";
        //商户网站订单系统中唯一订单号,必填
        
        //订单名称
        private $subject = "";
        //必填
        
        //付款金额
        private $total_fee = "";
        //必填
        
        //请求业务参数详细
        private $req_data = "";
        //必填
        
        //配置
        private $alipay_config = array();
        
    /************************************************************/
  2. 向支付宝申请新订单,并获取订单的token。
    请求订单token需要的req_data

    请求token的service为: alipay.wap.trade.create.direct

    1. 构造参数:

      $para_token = array(
          "service" => "alipay.wap.trade.create.direct",
          //  合作者身份(partner ID)
          "partner" => trim($this->alipay_config['partner']),
          //  APP使用的是RSA,网页版使用的是MD5
          "sec_id" => trim($this->alipay_config['sign_type']),
          //  返回的数据格式
          "format"    => $this->format,
          //  版本号?
          "v" => $this->v,
          //  唯一的请求号
          "req_id"    => $this->req_id,
          //  请求参数
          "req_data"  => $req_data,
          //  字符集,一般为utf8即可。
          "_input_charset"    => trim(strtolower($this->alipay_config['input_charset']))
      );
    2. 将构造好的请求参数,进行处理,字典排序,拼接字符串,签名:

      $para_filter = paraFilter($para_temp);
      $para_sort = argSort($para_filter);
      $mysign = $this->buildRequestMysign($para_sort);
      //签名结果与签名方式加入请求提交参数组中
      $para_sort['sign'] = $mysign;
      return $para_sort;

      处理:过滤值为空的数据,过滤签名类型和签名。

      function paraFilter($para) {
          $para_filter = array();
          while (list ($key, $val) = each ($para)) {
              if($key == "sign" || $key == "sign_type" || $val == "")continue;
              else    $para_filter[$key] = $para[$key];
          }
          return $para_filter;
      }

      字典排序:

      /**
       * 对数组排序
       * @param $para 排序前的数组
     */
    function argSort($para) {
        ksort($para);
        reset($para);
        return $para;
    }
    ```
    签名:
    ```php
    /**
     * 生成签名结果
     * @param $para_sort 已排序要签名的数组
     * return 签名结果字符串
     */
    function buildRequestMysign($para_sort) {
        //把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串
        $prestr = createLinkstring($para_sort);
        $mysign = "";
        switch (strtoupper(trim($this->alipay_config['sign_type']))) {
            case "MD5" :
                //  MD5直接将密钥拼接在字符串后面再进行MD5加密。
                $mysign = md5Sign($prestr, $this->alipay_config['key']);
                break;
            case "RSA" :
                //  RSA则是先读取商户的私钥,再用该密钥对字符串进行加密。
                $mysign = rsaSign($prestr, $this->alipay_config['private_key_path']);
                break;
            case "0001" :
                $mysign = rsaSign($prestr, $this->alipay_config['private_key_path']);
                break;
            default :
                $mysign = "";
        }
        
        return $mysign;
    }
    ```
3.  用构造好的参数请求支付宝后台申请新订单:

    **注意:请求时,必须带上SSL证书。**

    ```php
    $sResult = getHttpResponsePOST($this->alipay_gateway_new, $this->alipay_config['cacert'],$request_data,trim(strtolower($this->alipay_config['input_charset'])));
    ```
    请求函数的实现:
    ```php
    /**
     * 远程获取数据,POST模式
     * 注意:
     * 1.使用Crul需要修改服务器中php.ini文件的设置,找到php_curl.dll去掉前面的";"就行了
     * 2.文件夹中cacert.pem是SSL证书请保证其路径有效,目前默认路径是:getcwd().'\\cacert.pem'
     * @param $url 指定URL完整路径地址
     * @param $cacert_url 指定当前工作目录绝对路径
     * @param $para 请求的数据
     * @param $input_charset 编码格式。默认值:空值
     * return 远程输出的数据
     */
    function getHttpResponsePOST($url, $cacert_url, $para, $input_charset = '') {
    
        if (trim($input_charset) != '') {
            $url = $url."_input_charset=".$input_charset;
        }
        $curl = curl_init($url);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);//SSL证书认证
        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);//严格认证
        curl_setopt($curl, CURLOPT_CAINFO,$cacert_url);//证书地址
        curl_setopt($curl, CURLOPT_HEADER, 0 ); // 过滤HTTP头
        curl_setopt($curl,CURLOPT_RETURNTRANSFER, 1);// 显示输出结果
        curl_setopt($curl,CURLOPT_POST,true); // post传输数据
        curl_setopt($curl,CURLOPT_POSTFIELDS,$para);// post传输数据
        $responseText = curl_exec($curl);
        //var_dump( curl_error($curl) );//如果执行curl过程中出现异常,可打开此开关,以便查看异常内容
        curl_close($curl);
        
        return $responseText;
    }
    ```
    处理支付宝返回的数据,并获取token。
    
    ```php
    //URLDECODE返回的信息
    $html_text = urldecode($html_text);
    //解析远程模拟提交后返回的信息
    $para_html_text = parseResponse($html_text);
    //获取request_token
    $request_token = $para_html_text['request_token'];
    ```
    parseResponse函数的实现:
    
    ```php
    /**
     * 解析远程模拟提交后返回的信息
     * @param $str_text 要解析的字符串
     * @return 解析结果
     */
    function parseResponse($str_text) {
        //以“&”字符切割字符串
        $para_split = explode('&',$str_text);
        //把切割后的字符串数组变成变量与数值组合的数组
        foreach ($para_split as $item) {
            //获得第一个=字符的位置
            $nPos = strpos($item,'=');
            //获得字符串长度
            $nLen = strlen($item);
            //获得变量名
            $key = substr($item,0,$nPos);
            //获得数值
            $value = substr($item,$nPos+1,$nLen-$nPos-1);
            //放入数组中
            $para_text[$key] = $value;
        }
        
        if( ! empty ($para_text['res_data'])) {
            //解析加密部分字符串
            if($this->alipay_config['sign_type'] == '0001') {
                $para_text['res_data'] = rsaDecrypt($para_text['res_data'], $this->alipay_config['private_key_path']);
            }
            
            //token从res_data中解析出来(也就是说res_data中已经包含token的内容)
            $doc = new DOMDocument();
            $doc->loadXML($para_text['res_data']);
            $para_text['request_token'] = $doc->getElementsByTagName( "request_token" )->item(0)->nodeValue;
        }
        
        return $para_text;
    }
    ```
  1. 携带token进行订单支付。

    成功请求token回来后,就可以向支付宝发出一次支付请求。

    同样构造请求数据:

    //业务详细只需要携带步骤2的token即可。
    $req_data = '<auth_and_execute_req><request_token>' . $request_token . '</request_token></auth_and_execute_req>';
    //必填
    
    //构造要请求的参数数组,无需改动
    $parameter = array(
        "service" => "alipay.wap.auth.authAndExecute",
        //  合作者身份(partner ID)
        "partner" => trim($this->alipay_config['partner']),
        //  签名类型
        "sec_id" => trim($this->alipay_config['sign_type']),
        //  和步骤2一致
        "format"    => $this->format,
        "v" => $this->v,
        "req_id"    => $this->req_id,
        //  业务详细参数
        "req_data"  => $req_data,
        //  字符集,一般为utf8.
        "_input_charset"    => trim(strtolower($this->alipay_config['input_charset']))
    );

    将这些参数,在页面中传送给支付宝即可发起一次支付请求。

    在PHP 中的实现就是将这些参数,渲染至HTML中,再将HTML中的表单提交即可。

    到此,网页版的支付宝支付完成整个流程。

支付结果异步通知

在上面,我们看到有两个参数传给了支付宝:

  • call_back_url: 交易成功后,支付宝页面上“返回到商家页面”的地址(同步回调)
  • notify_url: 交易状态变更后,支付宝通知网站的回调地址(异步通知)
对于手机网站支付产生的交易,支付宝会根据原始支付API中传入的异步通知地址notify_url,通过POST请求的形式将支付结果作为参数通知到商户系统。

对于App支付产生的交易,支付宝会根据原始支付API中传入的异步通知地址notify_url,通过POST请求的形式将支付结果作为参数通知到商户系统。

支付宝异步通知官方文档中写的比较清楚,什么时候出发通知,返回什么参数,注意事项都有,开发者可以根据自己的情况查看具体信息。

验签步骤可以移步至这里

这里就简单的用手上的项目举例说明,支付宝通知后,后台是如何进行验签和处理订单。

public function app_notifyOp(){
    $payment_api = $this->_get_payment_api();
    $payment_config = $this->_get_payment_config();
    // 支付宝是用POST方式发送通知信息
    $callback_info = $payment_api->getNotifyInfoApp($_POST);
    if($callback_info) {
        //验证成功
        if ($callback_info['order_state']) {
            // 如果是支付成功则改变订单状态
            $result = $this->_update_order($callback_info['out_trade_no'], $callback_info['trade_no']);
        }else{
            // 如果是退款成功则修改退订的相关状态
            $result = $this->_app_refund($callback_info['out_trade_no'], $callback_info['trade_no'], $callback_info['refund_fee']);
        }
        if($result['state']) {
            echo 'success';die;
        }
    }
    //验证失败
    echo "fail";die;
}
  1. 获取支付宝通知数据
    支付宝异步通知是POST请求,返回的数据结构如下:

    {
        "total_amount": "31.00",
        "buyer_id": "ID",
        "trade_no": "TRADE_NO",
        "body": "pay_sn:580546601841783375",
        "notify_time": "2017-04-27 09:50:59",
        "subject": "580546601841783375",
        "sign_type": "RSA",
        "buyer_logon_id": "ID",
        "auth_app_id": "APPID",
        "charset": "utf-8",
        "notify_type": "trade_status_sync",
        "invoice_amount": "31.00",
        "out_trade_no": "580546601841783375_r",
        "trade_status": "TRADE_SUCCESS",
        "gmt_payment": "2017-04-27 09:50:58",
        "version": "1.0",
        "point_amount": "0.00",
        "sign": "SIGNATURE",
        "gmt_create": "2017-04-27 09:50:58",
        "buyer_pay_amount": "31.00",
        "receipt_amount": "31.00",
        "fund_bill_list": "[{&quot;amount&quot;:&quot;31.00&quot;,&quot;fundChannel&quot;:&quot;ALIPAYACCOUNT&quot;}]",
        "app_id": "APPID",
        "seller_id": "SELLERID",
        "notify_id": "8414394a1190f25edbbec9ba4b98642mem",
        "seller_email": "YOUR_ALIPAY_ACCOUNT"
    }
  2. 验签数据
    验签需要支付宝的公钥

    验签和签名的流程是一样的,都是将所有除了 sign 以外的参数,进行字典排序,并以 key=value 的形式以 & 符号拼成字符串,再使用密钥进行签名,将得到的签名与支付宝返回的签名进行对比,完成验签过程。

    function getSignVeryfy($para_temp, $sign) {
        //除去待签名参数数组中的空值和签名参数
        $para = paraFilter($para_temp);
        
        //对待签名参数数组排序
        $para = argSort($para);
    
        //把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串
        $prestr = createLinkstring($para);
        $prestr = htmlspecialchars_decode($prestr);
        $isSgin = false;
        switch (strtoupper(trim($this->alipay_config['sign_type']))) {
            case "MD5" :
                $isSgin = md5Verify($prestr, $sign, $this->alipay_config['key']);
                break;
            case "RSA" :
                $isSgin = rsaVerify($prestr, trim($this->alipay_config['ali_public_key_path']), $sign);
                break;
            case "0001" :
                $isSgin = rsaVerify($prestr, trim($this->alipay_config['ali_public_key_path']), $sign);
                break;
            default :
                $isSgin = false;
        }
        logResult($log);
        
        return $isSgin;
    }

    但是这里有个坑,就是返回数据中的 fund_bill_list 是经过html转义的(如例子中的数据: [{&quot;amount&quot;:&quot;31.00&quot;,&quot;fundChannel&quot;:&quot;ALIPAYACCOUNT&quot;}]),如果直接使用该参数进行签名,则会导致签名失败。这里就需要将字符串转义了: [{"amount":"31.00","fundChannel":"ALIPAYACCOUNT"}] ,用转义后的参数值进行签名,通过校验。

  3. 更改订单状态

验签完毕后,后台就可以根据实际情况进行订单状态的更改。

完毕

祝各位程序猿在开发支付宝支付时不再有坑,也希望支付宝在后续的更新中不再埋雷。

913 声望
24 粉丝
0 条评论
推荐阅读
Vue-cli3 简qian易yi教程
使用 cli 的插件,可以很快的搭建一个项目的结构。如 axios 的插件 vue-cli-plugin-axios,可以自动创建一个带有 request 和 resonpose 的拦截器的 axios 的实例的文件。使用时直接引入即可。

leungjz1阅读 4.1k评论 4

怎样用 PHP 来实现枚举?
在数学和计算机科学理论中,一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象的计数。这两种类型经常(但不总是)重叠。枚举是一个被命名的整型常数的集合,枚举在日常生活中很常见,...

唯一丶25阅读 6.3k评论 4

Git操作不规范,战友提刀来相见!
年终奖都没了,还要扣我绩效,门都没有,哈哈。这波骚Git操作我也是第一次用,担心闪了腰,所以不仅做了备份,也做了笔记,分享给大家。问题描述小A和我在同时开发一个功能模块,他在优化之前的代码逻辑,我在开...

王中阳Go5阅读 1.8k评论 2

封面图
微信公众号开发:自动回复文本/图片/图文消息/关键词回复/上传素材/自定义菜单
对接流程1、申请微信公众号测试账号URL:[链接]2、登录,配置开发者服务器URL和Token开发者服务器配置代码:config.php {代码...} URL是config.php在你服务器的URLToken是上面代码自己设置的Token搞定之后,就能完...

TANKING2阅读 10k

Hyperf 3.0 发布,PHP 新时代
在过去的一年半时间里,Hyperf 2.2 共发布了 35 个小版本,使 Hyperf 达到了一个前所未有的高度,这里也获得了一些不错的数据反馈。

huangzhhui3阅读 1k

封面图
多个著名 Go 开源项目被放弃,做大开源不能用爱发电,更不能只靠自己!
大家好,我是煎鱼。相信关注我的许多同学都有接触 Go 语言的开发,甚至在企业中多有实践。那么你在日常开发中,势必会接触到 gorilla 组织下的各个 Go 开源项目。如下图:gorilla/mux:Star:17.9k。a powerful r...

煎鱼1阅读 2.3k

Go for 循环有时候真的很坑。。。
大家好,我是煎鱼。不知道有多少 Go 的面试题和泄露,都和 for 循环有关。今天我在周末认真一看,发现了 redefining for loop variable semantics 。著名的硬核大佬 Russ Cox 表示他一直在研究这个问题,并表示十...

煎鱼阅读 3.4k

913 声望
24 粉丝
宣传栏