menu 光风霁月。
微信支付接入指引 ( 基于AWS云服务 )
351 浏览 | 2020-05-21 | 阅读时间: 约 6 分钟 | 分类: 资源访问 | 标签: 微信支付
请注意,本文编写于 154 天前,最后修改于 154 天前,其中某些信息可能已经过时。

前言

本文是基于 AWS 云服务的一次实践,对其他云服务接入也具有一定的参考价值。
关于 Node 接入微信支付,请在本站搜索我的另外一篇文章《微信支付接入指引(基于 Node Express)》

【重要通知】2020-05-21更新:关于微信小程序接入微信支付,微信官方在今年 5 月上旬推出云原生函数,不再需要原始的加密、解密校验等复杂繁冗的过程,同获取用户信息的云原生函数用法类似。具体可参考官方推送:点我直达

获取 lambda 函数的动态出口 IP

因微信支付接口中需要源请求IP地址用于安全校验,但是 lambda 是公共云服务,IP 是动态的,因此需要动态获取 IP 地址

重要说明

2020-05-22更新:
获取动态出口IP地址有更好的办法啦!不再需要下面的新建函数、操作 API Gateway,代码如下:

/**
 * 获取 Lambda 的动态 IP
 *
 * @returns
 */
function getLambdaExportIP() {
  return new Promise((resolve, reject) => {
    axios.get("http://checkip.amazonaws.com/")
      .then(res => { resolve(res.data) })
      .catch(err => { reject(err) });
  });
}

【弃】新建 lambda 函数, 命名为 getLambdaExportIP, 内部具体实现代码如下:

exports.handler = (event, context, callback) => {
  console.log(JSON.stringify(event));
  const response = {
    statusCode: 200,
    body: event.exportIP
  };
  callback(null, response);
};

【弃】打开 API Gateway, 选择已创建的 API ,新建资源 lambda, 同时创建类型为 GET 的方法

(注意: 在创建方法时, 需要绑定之前创建的名为 getLambdaExportIP 的函数),

【弃】并启用 CORS, 在 方法请求 处, 添加名为 x-forwarded-for 的标头,结果如下

【弃】在集成请求处, 选择 映射模板, 请求正文传递选择 未定义模板时, 新建模板, 并输入 application/json, 在模板正文中, 填入以下信息

{
    "exportIP" : "$input.params('x-forwarded-for')"
}

结果如下图所示

调用微信统一下单接口的准备

  • 统一下单接口参数 (只考虑 WEB 接入且只考虑必要的参数)

    详细请参考 => => 微信支付官方文档

    变量名必填类型示例值
    appidString(32)wxd678efh567hg6787
    mch_idString(32)1230000109
    device_infoString(32)013467007045764
    nonce_strString(32)5K8264ILTKCH16CQ2502SI8ZNMTM67VS
    signString(32)C380BEC2BFD727A4B6845133519F3AD6
    sign_typeString(32)MD5
    bodyString(128)腾讯充值中心-QQ会员充值
    detailString(6000)
    out_trade_noString(32)20150806125346
    total_feeInt88
    spbill_create_ipString(64)123.12.12.123
    notify_urlString(256)http://www.weixin.qq.com/wxpay/pay.php
    trade_typeString(16)JSAPI
    product_idString(32)12235413214070356458058

    对于一些固定的参数就不再赘述, 例如 (appid, mch_id)

    nonce_str, 是类型为String的32位以下的随机字符串, 关于其生成可以自己封装也可以采用别人开发好的包, 这里采用 string-random, 使用 npm 安装即可, 下面的包也是用同样的方法。

    out_trade_no, 订单 id, 可以采用 uuid.v4().substring(0, 32) 来生成, 由于官方要求位数小于 32, uuid 生成的字符串大于32位, 可以采用 substring()方法进行截取, 同样的, uuid 也需要安装。

    spbill_create_ip, 也就是一开始费尽波折得到的 lambda 函数的出口 IP, 微信支付后台在得到我们的请求后会校验请求标头 x-forwarded-for 与 这里的 spbill_create_ip,防止请求被劫持修改。

    notify_url, 用户支付成功后, 微信支付后台会异步推送此结果到这个 url, 后面会介绍具体的步骤。

    trade_type, WEB开发采用 NATIVE, 小程序或其他采用 JSAPI,具体需要可以参考微信官方文档主页的 => => 具体介绍

    product_id, 这个是产品 id, 此参数需要你的数据库拥有产品表, 此参数一般由前端传过来。

    sign, 签名验证, 需要根据微信的要求进行 MD5 运算, 具体实现后面介绍。

  • sign签名验证

    我已经实现好了, 代码可以复制即用, 如果想自己实现, 请参考 => => 官方说明

    请注意, 需要提前安装 md5

    let MD5 = require("md5");
    /**
     * 签名算法
     *
     * @param {Object} params
     * @returns
     */
    function sign(params, key) {
      let result = Object.keys(params).sort();
      let len = result.length;
      let stringA = result.reduce((temp, item, index) => {
        if (!params[item]) { return temp }
        return temp += `${item}=${params[item]}${index === len ? '' : '&'}`;
      }, "");
      return MD5(`${stringA}key=${key}`).toUpperCase();
    }

    参数key是商户的秘钥, params是前面表格内的参数

  • notify_url 实现

    打开 API Gateway, 在之前创建的资源 lambda 下再次新建资源, 名为 tenpay, 同时新建POST 方法, 绑定函数 acceptTencentPayResult (函数提前建一个即可), 函数的内容如下

    exports.handler = (event, context, callback) => {
        console.log(event);
        let backWords = "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
        callback(null, backWords);
    };

    如果返回的内容不符合格式或者微信后台接收不到返回值, 它会一直通知 (放心使用上面的代码, 这是成功的)。

    启用 CORS, 打开 集成请求, 找到映射模板, 新建 text/xml, 内容如下:

    {
        "xml" : "$util.escapeJavaScript($input.path('$'))"
    }

    关于API Gateway 设置完记得部署, 否则设置不会生效(这一点针对本文所有操作 !!! )

调用统一下单接口

使用 axios, x2js, md5

axios 是第三方包, 用于发送 httphttps 请求, 使用前需要自行安装

x2js 是第三方包, 用于把对象转换为 xml, 使用前需安装

使用方法为

 'use strict';
 let axios = require("axios");
 let x2js = require("x2js");
 let convert = new x2js(); // 这里需要 new 请注意
 let random = require("string-random");

开始调用, 这里使用

/**
  * 获取统一支付订单
  *
  * @param {Object} event
  * @returns
  */
function getUnifiedOrder(event) {
  let params = {
    "appid": event.appid,
    "body": "**网页-课程购买",
    "detail": "该笔订单用于购买**课程",
    "device_info": "WEB",
    "mch_id": event.mchId,
    "nonce_str": random(32),
    "notify_url": event.notifyUrl,
    "out_trade_no": event.orderId,
    "product_id": event.productId,
    "sign_type": "MD5",
    "spbill_create_ip": event.ip,
    "total_fee": event.fee,
    "trade_type": "NATIVE",
  };
 
  params.sign = sign(params, event.key);
 
  return new Promise((resolve, reject) => {
    let data = convert.js2xml({ xml: params });
    axios.post("https://api.mch.weixin.qq.com/pay/unifiedorder", data)
      .then(res => { resolve(res.data) })
      .catch(err => { reject(err) });
  });
}

返回结果(Content-Type 类型为 text/xml

由于涉及隐私, 我已对敏感信息打码。

使用 x2js 将返回的信息进行转换, 获得对象, 把 code_url 返回给前端渲染即可

数据库插入预订单

微信支付后台推送支付结果

推送的内容

实际内容

格式化后的内容

推送的内容是 text/xml 格式, 由于我在 API Gateway的设置, 返回的结果是字符串,需要你使用 x2js 转换为对象格式

修改数据库订单状态

订单状态获取

由于微信不保证推送结果能推送到我们自己的服务器, 所以不能依赖此消息来维持订单状态

比较好的一种办法如下:

用户支付完成之后点击 我已支付 检查数据库, 如果已支付, 打开相关资源的锁, 如果数据库没有支付, 则调用微信支付订单状态获取API, 获取订单支付结果

具体细节前面有类似步骤, 读者可以自行实现, 这里给出微信官方文档 => => 点我前往


微信小程序接入

因为微信小程序和网页版十分相似, 所以这里只描述不同的部分

openid 获取

小程序的支付方式为 NATIVE 而不是 JSAPI,因此需要 openid

  • 安装第三方包 axios
  • 请求代码如下:

    /**
     * 获取用户的 openid (小程序专用)
     *
     * @param {Object} event
     * @returns
     */
    function getUserOpenId(event) {
      let config = {
        params: {
          appid: event.appid,
          secret: event.secret,
          js_code: event.code,
          grant_type: "authorization_code"
        }
      };
      return new Promise((resolve, reject) => {
        axios.get("https://api.weixin.qq.com/sns/jscode2session", config)
          .then(res => { resolve(res.data) })
          .catch(err => { reject(err) });
      });
    }

再次签名

在网页版微信支付接入时, 微信返回一个 code-url 然后我们返回给前端用于二维码渲染, 在小程序接入时, 情况不再如此, 因为在调用统一下单接口时trade_type 字段需要由NATIVE 改为 JSAPI, 微信返回的结果也不同了。

微信返回的结果如下:

<xml>
    <return_code><![CDATA[SUCCESS]]></return_code>
    <return_msg><![CDATA[OK]]></return_msg>
    <appid><![CDATA[wx2421b1c4370ec43b]]></appid>
    <mch_id><![CDATA[10000100]]></mch_id>
    <nonce_str><![CDATA[IITRi8Iabbblz1Jc]]></nonce_str>
    <openid><![CDATA[oUpF8uMuAJO_M2pxb1Q9zNjWeS6o]]></openid>
    <sign><![CDATA[7921E432F65EB8ED0CE9755F0E86D72F]]></sign>
    <result_code><![CDATA[SUCCESS]]></result_code>
    <prepay_id><![CDATA[wx201411101639507cbf6ffd8b0779950874]]></prepay_id>
    <trade_type><![CDATA[JSAPI]]></trade_type>
</xml>

我们需要把这个结果中的一些字段再次签名然后返回给小程序 (前面的签名算法仍可用)。

变量名必填类型示例值
appIdStringwxd678efh567hg6787
timeStampString1490840662
nonceStrString5K8264ILTKCH16CQ2502SI8ZNMTM67VS
packageStringprepay_id=wx2017033010242291fcfe0db70013231072
signTypeStringMD5

再次签名时的字段即为上面5个字段, timeStampnonceStr 可以再次生成 也可以用这个结果的。

注意! package不是prepay_id 而是 prepay_id=****

注意! 再次签名时仍然需要商家的 key

这里建议返回给小程序的格式不要再是 xml 结构了, 因为小程序还需要再次解析, 直接返回 通用的json 格式即可, 返回给小程序一共包括 6 个字段, 上面表格 5 个以及生成的签名。

小程序调用 wx.requestPayment()

当小程序接收到返回的 6 个字段后, 调用 sdk 中的 API , 把必要参数填写完毕后,即可调用。

当小程序收到支付成功的结果之后, 建议让后台检查支付结果 (调用检查支付结果的API, 具体细节请查阅 => => 微信官方文档), 并修改相应数据库。建议小程序发送一些必要字段给后台来完成API调用, 如果不这样, 后台需要查数据库才能得到刚刚的订单信息。

总结与说明

如果您在试验时接收不到微信的支付结果推送, 请不要怀疑微信没有推送, 这里列出可能的几种原因供您排错:

  • 您的SNI 没有配置正确, 如果同一个服务器拥有多个域名, 微信可能推送不到正确的地址上面, 您可以查找相关的资料进行解决。
  • 您没有正确接收微信的推送, 正如本文上面的acceptTencentPayResult 函数使用的 API Gateway 的集成请求处设置为$input.eascapeJavaScript($input.path('$')) 作用就是把正文中的特殊字符进行转义, 如果只有 $input.path('$'), 我也接受不到 xml 正文。
  • 您可以只打印请求的头部 (headers), 不管正文, 如果能打印出请求的头部, 说明微信后台已经推送到正确的地址上了, 只是你没有正确接收而已。如果没有打印出来, 微信可能没有推送到正确的地址上。
  • 如果您查阅资料时看到某些帖子上说微信根本就没有推送, 请不要相信这种言论, 微信后台在尽力推送, 只是没有推送到正确的地址上而已。
  • 关于推送的地址是httpshttp , 我可以明确的告诉你两者都可以, 微信官方文档上说不需要SSL证书的意思就是可以有也可以没有,对此不作要求。如果您把https 改为http 时接收成功, 一定是相关的配置错误了,或者您的SSL证书过期了,微信后台认为不安全所以没有推送。
  • 如果您仍然解决不了问题, 可以到微信开放社区的微信支付版块那里提问, 那里会有很多与你问题相同或类似的情况, 你也可以主动提问请求他人帮助你解决问题。
  • 微信推送的正文类型是text/xml, 如果你的后台设置的允许接收的标头 (Content-Type) 里没有它, 也可能造成一定的错误。
  • 为了方便您进行有关测试, 我这里给出微信支付结果推送的头部(这是我某次试验的结果)

    { 
        host: 'snnu.henrenx.cn',
        'user-agent': 'Mozilla/4.0',
        'content-length': '837',
        accept: '*/*',
        'cache-control': 'no-cache',
        'content-type': 'text/xml',
        pragma: 'no-cache',
        'x-forwarded-for': '140.207.54.75',
        'accept-encoding': 'gzip',
        connection: 'close' 
    }
知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

发表评论

email
web

全部评论 (暂无评论)

info 还没有任何评论,你来说两句呐!