需求: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个参数需要填:
- private_key,商户私钥,位于文件
apiclient_key.pem
中。请见,商户API证书和什么是私钥?什么是证书?。 - mchid,商户号。
- serialNo,商户证书的证书序列号。请见什么是证书序列号。
填好之后,直接发个测试的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