微信支付V3版本的openresty实现与避坑指南(服务端)

需求:app接入微信支付,实现app内调起微信认证支付。

具体流程图官网有指导:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_5_0.shtml,本贴介绍的模式是直连商户充值流程图官网也有放出,这里也贴一下:

 

个人觉得第7步应该是没啥用途的,客户端app发起创建订单,这个也做好做些拦截,避免过多无效数据,没支付前,客户端发起的任何操作都没啥太大 意义。

服务端主要实现3个接口:创建订单(客户端发起,验证后,到微信侧再创建获取一个订单key),查询订单(客户端发起,在支付完成后,主动提示服务器核对订单,最好是在客户端支付完成延时一段时间,再轮询,成功则结束),通知回调(客户端向微信侧支付后,微信给服务端发起支付结果通知的接口)。

下面主要说下3个接口的主要核心代码和其中的坑:

写接口前要注意认真看完这个接口规则文档:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay-1.shtml

其中

  • 使用基于非对称密钥的SHA256-RSA的数字签名算法,不再使用MD5或HMAC-SHA256

这点要特别注意,每个请求头都必须要要认证的Authorization字段,

1.创建订单

官方文档链接:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_2_1.shtml

大致代码如下:

(一)创建订单:需要注意 function get_token(method, url, body)中的noncestr,官方文档给出的例子用的32位随机码,注意这是个大坑,这是个大坑,这是个大坑,实测是有问题的,建议直接用毫秒时间戳替代,不信的同学可以自己试下,关键微信侧返回的错误码永远都是提示你验签失败,你根本不可能根据提示得出是随机码出问题了,这个还真是我一个点一个点排查出来的。

local resty_rsa = require "resty.rsa"
-- git 地址:https://github.com/spacewander/lua-resty-rsa/tree/master/lib/resty


-- sha256 with rsa 加密算法
local function sha256_with_rsa(privatekey, str)
	local priv, err = resty_rsa:new({ private_key = privatekey, algorithm = 'SHA256' })
	if not priv then
		return nil, err
	end
	return priv:sign(str)
end

-- 获取签名
-- 参考文档:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml
-- method:操作方法:GET POST PUT  等 大写
-- url: 操作的地址
-- body:数据体,GET方法为空
local function get_token(method, url, body)
    local time = ngx.time()    -- 时间戳
    local noncestr = ngx.now() * 1000    -- 随机数取的毫秒时间戳
    local msg = method .. '\n' .. url .. '\n' .. time .. '\n' .. noncestr .. '\n' .. body .. '\n'    -- 加密串拼接
    local sign, err = ngx.encode_base64(sha256_with_rsa(wxpaykey, msg))
    if not sign then
        return nil,nil
    end
    -- mchid: 发起请求的商户(包括直连商户、服务商或渠道商)的商户号 mchid
    -- serialno: 商户API证书serial_no,用于声明所使用的证书
    return 'WECHATPAY2-SHA256-RSA2048 ' .. 'mchid="' .. mchid .. '",'
    .. 'nonce_str="' .. noncestr .. '",'
    .. 'timestamp="' .. time .. '",'
    .. 'serial_no="' .. serialno .. '",'
    .. 'signature="' .. sign .. '"'
end

-- 请求订单
local function request_pay_ticket(body)
    local httpc = http.new()
    local bodyjson = utils.json_encode(body)    -- json序列化
    local authorization = get_token('POST', '/v3/pay/transactions/app', bodyjson)
    return httpc:request_uri("https://api.mch.weixin.qq.com/v3/pay/transactions/app",{
		    method = "POST",
		    body = bodyjson,
		    headers = {
		        ["Content-Type"] = "application/json",
                ["Authorization"] = authorization,
                ["Accept"] = "application/json",
                ["User-Agent"] = "lua-resty-http",
		    }
	    })
end

local bill_no -- 订单编号,自己创建
local attach = utils.json_encode({payid = payid})  -- 附加数据,必须是json格式的字符串
local body = { appid = wxappid, mchid = mchid, description = des, out_trade_no = bill_no, amount = {total = fee, currency = 'CNY'}, 
                    notify_url = pay_back_url, attach = attach}
-- pay_back_url充值的回调url
local ret, err = request_pay_ticket(body)

   如果按照以上代码还是返回"message":"错误的签名,验签失败",那么可以按照下面这个流程排查问题:

1.检查各参数是否正确,一定要对着微信网站数据过一遍,不要拿中间数据对比,一定要对着微信网站数据过一遍,不要拿中间数据对比,一定要对着微信网站数据过一遍,不要拿中间数据对比。中间别人给的数据,可能已经变了,而没有告知你,这个会浪费不少时间。

2.验证证书是不是对的,这个我也遇到了,拿到的第一版证书也是有问题,幸好后面补发给我了,要不又不知道要卡多久。。证书验证的方法官网有例子,大概命令如下:

openssl x509 -noout -modulus -in wxpay_cert.pem
openssl rsa -noout -modulus -in wxpay_key.pem
两个结果一样就OK的。

3.加密算法可参考官网例子https://wechatpay-api.gitbook.io/wechatpay-api-v3/qian-ming-zhi-nan-1/qian-ming-sheng-cheng,但这个里面有坑,比如哪个随机数,一定不要过长,官网的32位,其实是有问题的,建议拿时间戳(毫秒)试,位数应该有要求。

4.如果还是返回"message":"错误的签名,验签失败",这个时候,你可以拿官网给的两个工具试。链接地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay6_0.shtml,一个是可以验签的,把自己拼的填进去然后比对工具跑出来的对不对,不对的话,就是加密算法有问题,对了,那就是有参数或者数据拼接格式不对,这个时候可以用第二个工具。第二个工具是个postman的配置,直接按照git上的readme流程走,就可以了,有3个参数需要填:

填好之后,直接发个测试的req,如果还是“错误的签名,验签失败”,那么基本就可以确定你上面3个参数有问题,因为中间计算签名都是微信自己算的,这个我自己测试过,是对的,还是靠谱的。这个时候就回到步骤1,老老实实核对参数,步骤2校验证书吧。

(二)查询订单:这个接口简单,用到的就是个签名码接口,同上。

-- 查询订单
local function query_order_bill_no(billno)
    local httpc = http.new()
    local qs = {
        ["mchid"] = mchid
    }
    local authorization = get_token('GET', '/v3/pay/transactions/out-trade-no/'..billno..'?mchid='..mchid, '')
    return httpc:request_uri("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/"..billno,{
		    method = "GET",
            query = qs,
            headers = {
              ["Content-Type"] = "application/x-www-form-urlencoded",
              ["Authorization"] = authorization,
              ["Accept"] = "application/json",
              ["User-Agent"] = "lua-resty-http",              
            }
	    })    
end

local ret,err = query_order_bill_no(billno)

(三)通知回调:注意aes_256_gcm哪个算法的参数,特别是加密串和tag的获取,还有一点就是代码里提供的aes加解密源码,里面有问题,这个aes_256_gcm加解密没有用到padding,所以EVP_DecryptFinal_ex这个接口不需要调用,否则,回解密失败,所以最简单的修改就是将源码aes.lua里这一行的调用屏蔽即可(源码287-289行),这也是个坑。

local aes = require "resty.aes"
--aes加解密算法 https://github.com/openresty/lua-resty-string/tree/master/lib/resty

function utils.aes_256_gcm(key, iv, aad, text, tag)
	local _cipher = aes.cipher(256,"gcm")
	local aes_default, err = aes:new(
		key,
		nil,
		aes.cipher(256,"gcm"),
		{iv = iv})
	if not aes_default then
		ngx.log(ngx.ERR, 'utils.aes_256_gcm error:'..err..',#key='..#key..',iv='..iv..',aad='..aad..',cipher.size/8='.._cipher.size/8)
		return nil, err
	end
	local decrypted, err = aes_default:decrypt(text, tag)	
	return decrypted, err
end
    
local ret_format = {code = 'SUCCESS', message = '成功'}
local r = utils.json_decode(req.body_raw)
if r.event_type ~= 'TRANSACTION.SUCCESS' then
    ngx.log(ngx.ERR, '[wxpay]cb notify not success, event_type='..r.event_type)
    return res:json(ret_format)
end
local nonce = r.resource.nonce      -- 非空的初始化向量 iv
local associated_data = r.resource.associated_data      -- aad额外的认证数据。
local key = cfg.APIv3secret
local ciphertext = r.resource.ciphertext
local algorithm = r.resource.algorithm  -- 加解密算法
ciphertext = ngx.decode_base64(ciphertext)
local text = string.sub(ciphertext, 1, #ciphertext-16)
local tag = string.sub(ciphertext, -16)     -- AEAD密码模式中的身份验证标签。 如果是错误的,验证失败,函数返回false.
local ret, err = aes_256_gcm(key, nonce, associated_data, text, tag)
if not ret then
    ngx.log(ngx.ERR, '[wxpay] aes_256_gcm error, txt:'..ciphertext..',nonce:'..nonce..',associated_data:'..associated_data..',key:'..key)
    if err then
        ngx.log(ngx.ERR, '[wxpay] aes_256_gcm err:'..err)
    end
    return res:json(ret_format)
end

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值