蚂蚁金服(支付宝)支付对接 Alipay SDK with NodeJS(双向验证)

3,201 阅读5分钟

项目需要对接(阿里)蚂蚁金服和(腾讯)财付通的收款。业务场景比较简单,就是在网页上产生一个付款二维码,分别使用微信或者支付宝App扫码完成付款即可(支付宝:当面付业务;微信:JSAPI支付业务)。不得不说,微信在这一块的文档是比较完整的,对接很顺利的就完成了。

但是在接入支付宝时遇到了一些问题。因为项目使用NodeJS开发,在这方面蚂蚁金服对待NodeJS对态度相比Java、.NET、PHP和Python起来,似乎就是小娘养的,所有SDK对使用案例均未提及,仅仅提供了一个npm的一来包和源码地址。前往源码github查看,也只有聊聊几句说明和一个在后端根本无法运行的demo代码。

而搜索NodeJS接入支付宝的内容,仅有的几个案例几乎都是自己封装了部分功能的代码的残次品。无奈之下,只有两条路可走。要么自己写一套调用逻辑,要么试试看使用官方的Node版本SDK。

抱着不重复造轮子的观念,先克隆了官方SDK研究。好在项目中有测试代码,连蒙带猜的开始了一波操作。

密钥设置

1. 支付宝官方推荐RSA2,于是先用openssl生成私钥和公钥

openssl
OpenSSL> genrsa -out alipay_pri.pem 2048  # 私钥RSA2
OpenSSL> rsa -in alipay_pub.pem -pubout -out app_public_key.pem # 公钥
OpenSSL> exit

2. 登录蚂蚁金服,进入开发者中心,选中相关应用,在应用信息中的开发设置接口加签方式中设置公钥,将刚才生成的文件内容粘贴到文本框保存即可。再将支付宝公钥保存到本地文件alipay_key.pem(或者直接写到程序中)。

项目设置

3. 在项目中安装alipay-sdk包:

npm i -s alipay-sdk

const AlipaySdk = require('alipay-sdk').default
const fs = require('fs')

const alipaySdk = new AlipaySdk({
  appId: '20191017684131213',
  privateKey: fs.readFileSync('./alipay_pri.pem', 'utf8'), // 你生成的私钥
  alipayPublicKey:fs.readFileSync('./alipay_key', 'utf8') // 支付宝的公钥(不是你生成的那个公钥,是支付宝的那个!)
})

根据分析源码,引入的方式如上,仅需配置appId、私钥(用于加密)、回调地址即可。

4. 调用预创建订单业务。

具体的参数参考:官方文档。(画外音:这类文档一看就是出自理工男之手:当我看到“公共参数”和“请求参数”时,犹如自己是个外国人,每个字每个词都认识,但是组合在一起就不懂了)

后来看了源码才知道:公共参数是指在调用api时都会带上的参数,跟使用那项业务无关,其部分存在于url中都参数(诸如appId,业务名等,详见源码alipay.js:60,urlArgs)。请求参数则是指请求某个业务API,以POST方式,在form中的参数(诸如:bizContent,notify_url等)。其实作为开发者,更需要关心等是实例怎么创建,参数怎么配就行了。以生成预支付订单为例:

alipaySdk.exec('alipay.trade.precreate', {
  notify_url: global.PAY_RECALL_URL_ALIPAY, // 支付完成通知地址
  bizContent: {
    out_trade_no: 'LEADLEO_010341_1',
    subject: '头豹报告付费',
    goods_detail: [{
      goods_id: '1eca8621s',
      goods_name: '《市场行业概览》',
      quantity: 1,
      merchant_order_no: 'LEADLEO_010341',
      qr_code_timeout_express: '30m',
      price: 2000
    }],
    body: '头豹报告购买',
    total_amount: '0.01'
  }// 业务(API)参数
}, { log: { info: console.log } }).then(r => {
  console.log('res', r)
}).catch(er => {
  console.log('err', er)
})

exec方法接受3个参数:

  • 第一个参数:字符串,具体业务
  • 第二个参数:POST的Form内容,具体某个API的参数在bizContent字段中(JSON格式,这点比微信号,微信是XML),还可能包括一些“公共参数”,可参考相关业务的文档(请求参数部分)。
  • 第三个参数:配置类参数,注入log对象,包括info方法和error方法,可以是 console.log和console.error,或者上传的文件(必须是formData格式)

5. 处理返回

除非是通信过程的异常和签名错误,否则都是正常(then中)的返回。返回对象参考具体业务文档。

消息验证

当用户完成支付后,会收到来自支付宝当消息通知:POST方式提交到你调用时提供的notify_url地址。你可以不对其真实性进行验证,而直接执行业务逻辑(那样的话,实例化时的那个构造函数中就不需要alipayPublicKey参数)。不过强烈建议还是需要验证一下的。我们先看看送来的对象:

 {
  pp_id: "2019101768409753",
  auth_app_id: "2019101768409753",
  body: "头豹报告购买",
  buyer_id: "2088002005278604",
  buyer_logon_id: "anu***@gmail.com",
  buyer_pay_amount: "0.01",
  charset: "utf-8",
  fund_bill_list: '[{amount:"0.01",fundChannel:"ALIPAYACCOUNT"}]',
  gmt_create: "2020-01-17 15:47:23",
  gmt_payment: "2020-01-17 15:47:27",
  invoice_amount: "0.01",
  notify_id: "2020011700222154727078601406465216",
  notify_time: "2020-01-17 15:47:27",
  notify_type: "trade_status_sync",
  out_trade_no: "TEST_S6gJLiy1nfNnrCE",
  point_amount: "0.00",
  receipt_amount: "0.01",
  seller_email: "zhang.hui@frostchina.com",
  seller_id: "2088631530585833",
  sign: "ha4inINyICBKLMiCID/sqEITrrHlzZZacTFmgvcV3FYs5EucA81rZlqagwgzeK+SXarXxd0Qa9+1y/0b5JD/YL5wNti2KoiwfhDF9llvNZx2W/xhfbZNdykenmnMUSpUszyH2bp4FbWr8+25j8rexz26prG1SbDkslCJXVqBvyLoDxHDnaUhkvKDU4yWpVIfGipMROmyfeYqzRbHzHhXfAemSfus0toDXvVlW1gtcJOlWhP3qugmNgXdJ5CfPHKGqhE4Kdg2g5Mzw0nkSSZUV28OQKkEUXWhSeM+xZPWgyO6PHfvmSmeZ4zwGbbN8pV6u95A+aXyoEOmOdeBcLCzbA==",
  sign_type: "RSA2",
  subject: "头豹报告付费",
  total_amount: "0.01",
  trade_no: "2020011722001478601439721848",
  trade_status: "TRADE_SUCCESS",
  version: "1.0"
}

6 验证消息可靠。以express为例,代码如下:

// 支付宝支付通知
router.post('/alipayPayed', async (req, res) => {
  try {
    // 验证是否合法
    if (alipayApi.checkNotifySign(req.body)) {
      // 具体业务逻辑
      res.send('success')
    } else {
      console.error('支付成功回调消息未通过验证')
      
    }
  } catch (err) {
    console.error(err)
    res.send('error')
  }
})

截止目前的版本,3108aa3,作者未将验证过程中的调试信息去除。可以修改位于node_modules/alipay-sdk/lib/alipay.js:178 console.log代码注释。