拿到手的是 Laravel 5.2.* 版本的小程序后台项目源码,在借助运维的脚本搭建好服务器环境,部署代码并恢复了基本的数据库,修复些许报错之后,摆在面前的工作就是接通小程序支付模块。

微信小程序支付的证书可以直接拿到证书压缩包,解压之后替换就好了。项目中使用了 pem 证书类型,而 p12 证书类型在 java 中用的比较多。因为支付宝小程序支付之前并未接触过,所以只能从它的文档出发。这其中就不免遇到很多问题。

与微信支付的常规流程基本一致,支付宝小程序支付流程如下:

第四步:调用接口 - 支付宝小程序支付
第四步:调用接口 - 支付宝小程序支付

小程序应用向商户服务端(我们的后台 API 服务)发起支付请求,商户服务器再向支付宝服务端发起支付请求,携带小程序端的用户信息和订单信息,以获取支付宝服务端返回的交易号 tradeNo (类似微信支付的预支付ID prepayid)。之后将交易号参数返回小程序端,让其唤起支付宝收银台,完成支付请求。

支付成功后,小程序端会同步收到支付结果,而商户服务端收到异步的支付结果,再去完成处理订单状态等后续逻辑。

因为这是一个完整的项目,所以前面的用户注册、支付宝登录获取 user_id (作用同微信小程序里的用户 open_id),包括接入支付宝支付的 SDK 等流程都不需要去管了。表面上只需要去申请相关的支付证书,并替换即可。

关于 SDK 版本

原版本 SDK 版本号 alipay-sdk-php-20161101,过于老旧,怕出现未知错误,直接从 alipay / alipay-sdk-php-all - github 下载了最新版本 alipay-sdk-PHP-4.11.14.ALL,解压、替换上去。没有太大的问题。

还有一个 服务端 SDK(Easy 版),调用明显更加的简洁(不清楚是否用了什么 php7 新特性),已经可以通过 composer 项目依赖下载了。因为时间关系,没有去尝试接入。

加签方式的选择

接口加签方式配置说明

加签就是支付中的签名生成方式,主要分为 公钥模式公钥证书 方式。

公钥模式 是将通过 支付宝开放平台开发助手 工具生成的应用公钥提交到支付后台,然后支付宝会生成相应的支付宝公钥。主要使用参数:APPID、应用私钥、支付宝公钥,这些参数都是一行字符串。

加签方式设置地址: 密钥管理,开放平台密钥管理 - 接口加签方式

公钥证书 则是通过 支付宝开放平台开发助手 工具生成的 CSR 文件(包含应用公钥信息)和应用公钥、应用私钥,提交CSR 文件到密钥管理中,然后支付宝会生成相应的 应用证书支付宝公钥证书支付宝根证书。主要参数就是APPID,三个证书和应用私钥。证书是文件,而应用私钥是一行字符串。

两者在使用场景上,涉及资金支出的场景需要使用到 公钥证书,包括 现金红包转账到支付宝账户 功能,对于目前的小程序的使用区别不大。

密钥字串和密钥文件的使用和转换

因为代码中引入了 pem 文件,让我误以为代码使用的是 公钥证书 方式。其实只要参考 PHP SDK 集成示例 文档,对比需要的参数就不会犯错。因为使用了错误的加签方式,所以参数会对应不上,勉强替换之后支付也不会成功,报错:商家订单参数异常,请重新发起付款

实际代码中使用的是 公钥模式。在察觉到不对劲并更换加签方式后,在去对应参数就很顺畅了。但 应用私钥支付宝公钥 存储在文件中,这与 PHP SDK 集成示例 中普通示例参数直接赋值密钥字串似乎不一致。

$c = new AopClient;
$c->gatewayUrl = "https://openapi.alipay.com/gateway.do";
$c->appId = "app_id";
$c->rsaPrivateKey = '请填写开发者私钥去头去尾去回车,一行字符串' ;
$c->format = "json";
$c->charset= "GBK";
$c->signType= "RSA2";
$c->alipayrsaPublicKey = '请填写支付宝公钥,一行字符串';
...

rsaPrivateKeyalipayrsaPublicKey 对应的都是密钥字串,而原支付接口中引入是密钥文件地址。经过对比和 AopClient 文件源码解读,弄清楚了其中的蹊跷。

...
class AopClient
{
    //应用ID
    public $appId;

    //私钥文件路径
    public $rsaPrivateKeyFilePath;

    //私钥值
    public $rsaPrivateKey;

    //网关
    public $gatewayUrl = "https://openapi.alipay.com/gateway.do";
    //返回数据格式
    public $format = "json";
    //api版本
    public $apiVersion = "1.0";

    // 表单提交字符集编码
    public $postCharset = "UTF-8";

    //使用文件读取文件格式,请只传递该值
    public $alipayPublicKey = null;

    //使用读取字符串格式,请只传递该值
    public $alipayrsaPublicKey;
...

原支付接口中引入是密钥所在文件地址赋值的参数变量是 $rsaPrivateKeyFilePath$alipayPublicKey。毫无疑问后者会对理解和区分 支付宝公钥支付宝公钥文件地址 造成困扰,为什么后者不是以 FilePath 结尾?鬼知道。

通过签名 sign() 和验签 verify() 方法,了解到 aopClient 对象会先判断对应的应用私钥文件地址和支付宝公钥文件地址是否存在,存在就读取文件内容,不存在则通过字符串拼接方式,将密钥字串添加上 -----BEGIN PUBLIC KEY----- 头部和 -----END PUBLIC KEY----- 尾部(这对头尾对应公钥文件,私钥 PUBLIC 替换成 RSA PRIVATE)。用 wordwrap() 函数处理密钥字串,进行换行(包含 \n 换行符在内,每行 65 个字符),用 openssl_get_publickey()openssl_get_privatekey() 从秘钥文件内容中获取密钥值。

从预期结果来看,换行处理后的密钥值 + 首尾注释 = pem 证书文件内容

所以,直接用密钥字符串替换原来 pem 文件内容(保留首、尾注释部分)即可,aopClient 拿到密钥会自行换行。

小程序创建交易订单

小程序支付用到 统一收单交易创建接口,里面的请求示例参数太过宽泛。因为原支付请求对应的 APP 支付,biz_content 中还包含了一个 product_code 参数,这就导致了之后的 Business FailedACCESS_FORBIDDEN 报错。

没有一个小程序支付的 demo 显然是很糟糕的体验。创建交易订单 - 小程序支付 里的示例代码是 java 版本的,但这已经可以给一些提示了。在去除了 product_code 之后,报错就消失了。也就是说,小程序支付并不需要额外地开通能力。

所需的四个参数 out_trade_no 是服务器创建的订单编号,total_amount 是支付金额,subject 是支付标题,buyer_id 是买家支付宝用户ID,这个参数存储在用户表中,可以通过 Auth::User()->user_id 拿到。

当返回数据存在支付宝交易号 trade_no,就意味着服务端的问题已经解决。

AES 加密、解密与 RSA2 签名、验签

如果需要对传送数据进行加密处理,可以在 密钥管理接口内容加密方式 里设置 AES 密钥和加密方式(暂未使用过),当然在实例化 aopClient 对象时也要设置 $encryptKey$encryptType 属性。

回顾 RSA 公钥、私钥使用的整个过程,可以看到商户服务端与支付宝服务端之间的数据交互基本面。

各自的公钥都有在对方存储,商户服务端在发送数据到支付宝服务端时,先用 AES 密钥对关键数据进行加密(如果开启的话),再使用应用私钥将所有参数生成签名;在得到返回数据时,先使用支付宝公钥进行验签,再用 AES 密钥对关键数据进行解密(同上),得到关键数据。

同理,支付宝服务端也有类似的操作。在发送数据时,先加密,再签名;在得到数据时,先验签,再解密。

当然,这些在使用支付宝支付 SDK 后,对开发者是透明的,不需要过多关注。