支付开发填坑记之支付宝

2,506 阅读7分钟
原文链接: segmentfault.com

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

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支付的详细请求参数: 点击查看

  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端将拼接好的字符串拿去请求支付宝客户端即可调起支付宝进行支付。拼接好的字符串大致如下图所示:

网页版支付

网页版支付步骤为:

  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的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 排序前的数组
       * return 排序后的数组
       */
      function argSort($para) {
          ksort($para);
          reset($para);
          return $para;
      }

      签名:

      /**
       * 生成签名结果
       * @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证书。

      $sResult = getHttpResponsePOST($this->alipay_gateway_new, $this->alipay_config['cacert'],$request_data,trim(strtolower($this->alipay_config['input_charset'])));

      请求函数的实现:

      /**
       * 远程获取数据,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。

      //URLDECODE返回的信息
      $html_text = urldecode($html_text);
      //解析远程模拟提交后返回的信息
      $para_html_text = parseResponse($html_text);
      //获取request_token
      $request_token = $para_html_text['request_token'];

      parseResponse函数的实现:

      /**
       * 解析远程模拟提交后返回的信息
       * @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;
      }
  3. 携带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. 更改订单状态
    验签完毕后,后台就可以根据实际情况进行订单状态的更改。

完毕

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