微信支付--V3接口
小程序支付
前提:前端獲取微信用戶的臨時code值給到后端稍途,后端根據(jù)code值調(diào)用微信API獲取openid索绪,拿到用戶唯一標(biāo)識openid斤贰;
流程:
1佑淀、后端首先調(diào)用JSAPI下單接口進行預(yù)下單,此接口參數(shù)中需指定一個通知地址拇颅,然后返回一個預(yù)支付交易會話標(biāo)識給到前端
2奏司、前端使用小程序調(diào)起支付接口調(diào)起支付
3、微信平臺獲取預(yù)下單時指定的通知地址并將支付結(jié)果通過該通知地址返回給我們
注:V3所有接口都需要做簽名處理樟插,api v3秘鑰和api 秘鑰不是同一個
小程序調(diào)起支付的參數(shù)需要按照簽名規(guī)則進行簽名計算:
1韵洋、構(gòu)造簽名串
簽名串一共有四行,每一行為一個參數(shù)黄锤。行尾以\n(換行符搪缨,ASCII編碼值為0x0A)結(jié)束,包括最后一行鸵熟。 如果參數(shù)本身以\n結(jié)束副编,也需要附加一個\n
小程序appId
時間戳
隨機字符串
訂單詳情擴展字符串
2、計算簽名值
使用商戶私鑰對*待簽名串*進行SHA256 with RSA簽名流强,并對簽名結(jié)果進行*Base64編碼*得到簽名值痹届。
命令行演示如何生成簽名
$ echo -n -e \
"wx8888888888888888\n1414561699\n5K8264ILTKCH16CQ2502SI8ZNMTM67VS\nprepay_id=wx201410272009395522657a690389285100\n" \
| openssl dgst -sha256 -sign apiclient_key.pem \
| openssl base64 -A
uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==
業(yè)務(wù)流程圖
api v3證書與秘鑰使用
# -*- coding: utf-8 -*-
import json
import time
import requests
import base64
import rsa
import os
import sys
import random
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
WXPAY_APPID="APPID"
WXPAY_APPSECRET="小程序appsecret"
WXPAY_MCHID="商戶號"
WXPAY_APIV3_KEY="API v3秘鑰"
WXPAY_NOTIFYURL="微信支付結(jié)果回調(diào)接口"
WXPAY_SERIALNO="商戶證書序列號"
WXPAY_CLIENT_PRIKEY="商戶私鑰"
WXPAY_PAY_DESC="商品描述(統(tǒng)一下單接口用到)"
def calculate_sign(client_prikey, data):
"""
簽名; 使用商戶私鑰對待簽名串進行SHA256 with RSA簽名,并對簽名結(jié)果進行Base64編碼得到簽名值
:param client_prikey: 商戶私鑰
:param data: 待簽名數(shù)據(jù)
:return :加簽后的數(shù)據(jù)
"""
with open(client_prikey, "r") as f:
pri_key = f.read()
private_key = rsa.PrivateKey.load_pkcs1(pri_key.encode('utf-8'))
sign_result = rsa.sign(data.encode('utf-8'), private_key, "SHA-256")
content = base64.b64encode(sign_result)
return content.decode('utf-8')
def decrypt(apikey, nonce, ciphertext, associated_data):
"""
證書和回調(diào)報文解密
:param apikey: API V3秘鑰
:param nonce: 加密使用的隨機串初始化向量
:param ciphertext: Base64編碼后的密文
:param associated_data: 附加數(shù)據(jù)包(可能為空)
:return :解密后的數(shù)據(jù)
"""
key = apikey
key_bytes = str.encode(key)
nonce_bytes = str.encode(nonce)
ad_bytes = str.encode(associated_data)
data = base64.b64decode(ciphertext)
aesgcm = AESGCM(key_bytes)
return aesgcm.decrypt(nonce_bytes, data, ad_bytes).decode('utf-8')
def random_str(lengh=32):
"""
生成隨機字符串
:return :32位隨機字符串
"""
chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
rand = random.Random()
return "".join([chars[rand.randint(0, len(chars) - 1)] for i in range(lengh)])
class WeXin(object):
def __init__(self, appid, mchid, secret, apikey, notify_url, client_prikey, serialno, pay_desc, user_agent=""):
self.appid = appid # APPID
self.mchid = mchid # 商戶號
self.secret = secret # 小程序appsecret
self.apikey = apikey # API v3秘鑰
self.notify_url = notify_url # 支付結(jié)果回調(diào)接口
self.client_prikey = client_prikey # 商戶私鑰
self.serialno = serialno # 商戶證書序列號
self.pay_desc = pay_desc # 商品描述
self.headers = {
"Content-Type": "application/json",
"Accept": "application/json",
"User-Agent": user_agent,
"Authorization": ""
}
def getOpenid(self, code):
"""獲取用戶openid"""
url = "https://api.weixin.qq.com/sns/jscode2session"
params = {
"appid": self.appid, # 小程序id
"secret": self.secret, # 小程序 appSecret
"js_code": code, # 登錄時獲取的 code
"grant_type": "authorization_code" # 授權(quán)類型打月,此處只需填寫 authorization_code
}
self.headers["Authorization"] = self.create_sign("GET", "/sns/jscode2session", "")
res = requests.get(url=url, params=params, headers=self.headers)
try:
openid = res.json()["openid"]
msg = "ok"
except:
openid = ""
msg = res.json()
return openid, msg
def jsapi(self, openid, amount, orderno, timestamp, randoms, time_expire="", attach="", goods_tag="", detail={}, scene_info={}, settle_info={}):
"""JSAPI下單"""
jsapi_url = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi"
body = {
"appid": self.appid,
"mchid": self.mchid,
"description": self.pay_desc, # 商品描述
"out_trade_no": orderno, # 商戶訂單號
"notify_url": self.notify_url, # 通知地址
"amount": {
"total": amount,
"currency": "CNY"
}, # 訂單總金額和貨幣類型{"total": 100, "currency": "CNY"}
"payer": {
"openid": openid
} # 支付者信息
}
if time_expire:
body["time_expire"] = time_expire
if attach:
body["attach"] = attach
if goods_tag:
body["goods_tag"] = goods_tag
if detail:
body["detail"] = detail
if scene_info:
body["scene_info"] = scene_info
if settle_info:
body["settle_info"] = settle_info
self.headers["Authorization"] = self.create_sign("POST", "/v3/pay/transactions/jsapi", json.dumps(body), timestamp, randoms)
res = requests.post(jsapi_url, json=body, headers=self.headers)
prepay_id = res.json().get("prepay_id")
return prepay_id
def payapi(self, prepay_id):
"""
小程序調(diào)起支付API
1队腐、通過JSAPI下單接口獲取到發(fā)起支付的必要參數(shù)prepay_id,然后使用微信支付提供的小程序方法調(diào)起小程序支付
"""
timeStamp = str(int(time.time()))
nonceStr = random_str()
package = "prepay_id=" + str(prepay_id)
data = WXPAY_APPID + "\n" + timeStamp + "\n" + nonceStr + "\n" + package + "\n"
sign = calculate_sign(self.client_prikey, data)
params = {
"timeStamp": timeStamp,
"nonceStr": nonceStr,
"package": package,
"signType": "RSA",
"paySign": sign
}
return params
def payquery(self, query_param, wx=False):
"""
查詢訂單
1奏篙、查詢訂單狀態(tài)可通過微信支付訂單號或商戶訂單號兩種方式查詢
:param query_param: 微信支付訂單號或商戶訂單號
"""
if wx:
# 微信支付訂單號查詢
query_url = "https://api.mch.weixin.qq.com/v3/pay/transactions/id/"
else:
# 商戶訂單號查詢
query_url = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/"
params = {
"mchid": self.mchid
}
url_path = query_url.split(".com")[1] + query_param + "?mchid=" + self.mchid
url = query_url + query_param
self.headers["Authorization"] = self.create_sign("GET", url_path, "")
res = requests.get(url=url, params=params, headers=self.headers)
return res.json()
def payclose(self, out_trade_no):
"""
關(guān)閉訂單
1柴淘、商戶訂單支付失敗需要生成新單號重新發(fā)起支付,要對原訂單號調(diào)用關(guān)單报破,避免重復(fù)支付;
2千绪、系統(tǒng)下單后充易,用戶支付超時,系統(tǒng)退出不再受理荸型,避免用戶繼續(xù)盹靴,請調(diào)用關(guān)單接口。
:param out_trade_no: 商戶訂單號
"""
url_path = "/v3/pay/transactions/out-trade-no/{}/close".format(out_trade_no)
url = "https://api.mch.weixin.qq.com" + url_path
body = {
"mchid": self.mchid
}
self.headers["Authorization"] = self.create_sign("POST", url_path, json.dumps(body))
res = requests.post(url=url, data=body, headers=self.headers)
return res.json() # 正常返回為204
def getCert(self, base_dir):
"""
獲取微信支付平臺證書列表
:param base_dir :指定生成證書的存放路徑
"""
url = "https://api.mch.weixin.qq.com/v3/certificates"
self.headers["Authorization"] = self.create_sign("GET", "/v3/certificates", "")
res = requests.get(url=url, headers=self.headers)
res_code = res.status_code
res_body = res.json()
# print(res_body)
if res_code != 200:
print("獲取公鑰證書失敗")
print(res_body)
return False
for i in range(0, len(res_body.get("data"))):
serial_no = res_body["data"][i]["serial_no"]
nonce = res_body["data"][i]["encrypt_certificate"]["nonce"]
ciphertext = res_body["data"][i]["encrypt_certificate"]["ciphertext"]
associated_data = res_body["data"][i]["encrypt_certificate"]["associated_data"]
data = decrypt(self.apikey ,nonce, ciphertext, associated_data)
wxcert_dir = os.path.join(base_dir, "key", serial_no)
if not os.path.isdir(wxcert_dir):
os.mkdir(wxcert_dir)
wxcert_file = os.path.join(wxcert_dir, "wxp_cert.pem")
with open(wxcert_file, "w") as f:
f.write(data)
return True
def create_sign(self, method, url, body, timestamp="", randoms=""):
"""
構(gòu)造簽名串
1、微信支付API v3通過驗證簽名來保證請求的真實性和數(shù)據(jù)的完整性稿静。
2梭冠、商戶需要使用自身的私鑰對API URL、消息體等關(guān)鍵數(shù)據(jù)的組合進行SHA-256 with RSA簽名
3改备、請求的簽名信息通過HTTP頭Authorization 傳遞
4控漠、簽名生成指南:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml
:param client_prikey: 商戶私鑰
:param mchid: 商戶號
:param serialno: 商戶證書序列號
:param method: 請求方式
:param url: 請求url,去除域名部分得到參與簽名的url,如果請求中有查詢參數(shù),url末尾應(yīng)附加有'?'和對應(yīng)的查詢字符串
:param body: 請求體
:return : authorization
"""
if not timestamp:
timestamp = str(int(time.time()))
if not randoms:
randoms = random_str()
sign_list = [method, url, timestamp, randoms, body]
sign_str = "\n".join(sign_list) + "\n"
signature = calculate_sign(self.client_prikey, sign_str)
authorization = 'WECHATPAY2-SHA256-RSA2048 ' \
'mchid="{0}",' \
'nonce_str="{1}",' \
'signature="{2}",' \
'timestamp="{3}",' \
'serial_no="{4}"'.\
format(self.mchid,
randoms,
signature,
timestamp,
self.serialno
)
return authorization
weixinPayV3 = WeXin(WXPAY_APPID, WXPAY_MCHID, WXPAY_APPSECRET, WXPAY_APIV3_KEY, WXPAY_NOTIFYURL, WXPAY_CLIENT_PRIKEY, WXPAY_SERIALNO, WXPAY_PAY_DESC)
微信支付--V3之前接口
付款到零錢
前提:
1、商戶號已入駐90日且截止今日回推30天商戶號保持連續(xù)不間斷的交易悬钳。
2盐捷、登錄微信支付商戶平臺-產(chǎn)品中心,開通付款到零錢默勾。
限制條件:
1碉渡、不支持給非實名用戶打款
2、一個商戶默認(rèn)同一日付款總額限額10萬元母剥,給同一個實名用戶付款滞诺,單筆單日限額200/200元( 若商戶需提升付款額度,可在【商戶平臺-產(chǎn)品中心-付款到零錢-產(chǎn)品設(shè)置-調(diào)整額度】頁面進入提額申請頁面环疼,根據(jù)頁面指引提交相關(guān)資料進行申請)
# -*- coding: utf-8 -*-
import hashlib
import requests
from PIL import Image
import os
import re
WXPAY_APPID="APPID"
WXPAY_APPSECRET="小程序appsecret"
WXPAY_MCHID="商戶號"
WXPAY_API_KEY="API 秘鑰"
WXPAY_CLIENT_CERT="商戶證書"
WXPAY_CLIENT_KEY="商戶秘鑰(PKCS#8格式化后的密鑰格式)"
WXPAY_ADVICE_DESC="付款備注(付款到零錢接口用到)"
def random_str(lengh=32):
"""
生成隨機字符串
:return :32位隨機字符串
"""
chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
rand = random.Random()
return "".join([chars[rand.randint(0, len(chars) - 1)] for i in range(lengh)])
def dict_to_xml(params):
xml = ["<xml>", ]
for k, v in params.items():
xml.append('<%s>%s</%s>' % (k, v, k))
xml.append('</xml>')
return ''.join(xml)
def dict_to_xml2(params):
xml = ["<xml>", ]
for k, v in params.items():
xml.append('<%s><![CDATA[%s]]></%s>' % (k, v, k))
xml.append('</xml>')
return ''.join(xml)
def xml_to_dict(xml):
xml = xml.strip()
if xml[:5].upper() != "<XML>" and xml[-6:].upper() != "</XML>":
return None, None
result = {}
sign = None
content = ''.join(xml[5:-6].strip().split('\n'))
pattern = re.compile(r"<(?P<key>.+)>(?P<value>.+)</(?P=key)>")
m = pattern.match(content)
while m:
key = m.group("key").strip()
value = m.group("value").strip()
if value != "<![CDATA[]]>":
pattern_inner = re.compile(r"<!\[CDATA\[(?P<inner_val>.+)\]\]>")
inner_m = pattern_inner.match(value)
if inner_m:
value = inner_m.group("inner_val").strip()
if key == "sign":
sign = value
else:
result[key] = value
next_index = m.end("value") + len(key) + 3
if next_index >= len(content):
break
content = content[next_index:]
m = pattern.match(content)
return sign, result
class WeiXinPay(object):
def __init__(self, mch_appid, mchid, api_key):
self.api_key = api_key
self.url = "https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers"
self.params = {
"mch_appid": mch_appid,
"mchid": mchid
}
def update_params(self, kwargs):
self.params["desc"] = WXPAY_ADVICE_DESC
self.params["check_name"] = "NO_CHECK"
self.params.update(kwargs)
def post_xml(self):
sign = self.get_sign_content(self.params, self.api_key)
self.params["sign"] = sign
xml = dict_to_xml(self.params)
if self.params["sign"]:
del self.params["sign"]
response = requests.post(self.url, data=xml.encode('utf-8'), cert=(WXPAY_CLIENT_CERT, WXPAY_CLIENT_KEY))
return xml_to_dict(response.text)
def post_xml2(self):
sign = self.get_sign_content(self.params, self.api_key)
self.params["sign"] = sign
xml = dict_to_xml2(self.params)
if self.params["sign"]:
del self.params["sign"]
response = requests.post(self.url, data=xml.encode('utf-8'), cert=(WXPAY_CLIENT_CERT, WXPAY_CLIENT_KEY))
return xml_to_dict(response.text)
def get_sign_content(self, dict, apikey):
"""
微信付款接口為V2接口习霹,與V3接口規(guī)則不同,此函數(shù)用于V2接口的簽名處理
1秦爆、剔除值為空的參數(shù)序愚,并按照參數(shù)名ASCII碼遞增排序(字母升序排序)
2、將排序后的參數(shù)與其對應(yīng)值等限,組合成“參數(shù)=參數(shù)值”的格式爸吮,并且把這些參數(shù)用&字符連接起來,得到stringA
3望门、在stringA最后拼接上秘鑰key形娇,得到stringSignTemp
4、對stringSignTemp進行MD5運算筹误,然后將結(jié)果轉(zhuǎn)大寫桐早,得到簽名值
:param dict: 字典數(shù)據(jù)
:param apikey: API秘鑰
:return :
"""
data = "&".join(['%s=%s' % (key, dict[key]) for key in sorted(dict)])
if apikey:
data = '%s&key=%s' % (data, apikey)
return hashlib.md5(data.encode('utf-8')).hexdigest().upper()
@staticmethod
def getAccessToken():
"""
接口調(diào)用憑證
"""
url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={}&secret={}"
appid = WXPAY_APPID
secret = WXPAY_APPSECRET
url = url.format(appid, secret)
res = requests.get(url)
try:
access_token = res.json().get("access_token")
except:
access_token = ""
return access_token
@staticmethod
def createQRCode(path, width):
"""
獲取小程序二維碼,適用于需要的碼數(shù)量較少的業(yè)務(wù)場景厨剪。通過該接口生成的小程序碼哄酝,永久有效,有數(shù)量限制
"""
url = "https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode?access_token={}"
body = {
"path": path,
"width": width
}
access_token = WeiXinPay.getAccessToken()
if not access_token:
return False, "獲取access_token失敗"
url = url.format(access_token)
res = requests.post(url=url, json=body)
# with open("./test.png", "wb") as f:
# f.write(res.content)
return res.headers, res.content
@staticmethod
def imgSecCheck(filepath, imgcheck_dir):
"""
檢驗一張圖片是否含有違法違規(guī)內(nèi)容
"""
url = "https://api.weixin.qq.com/wxa/img_sec_check?access_token={}"
access_token = WeiXinPay.getAccessToken()
if not access_token:
return False, "獲取access_token失敗"
url = url.format(access_token)
size = (500, 500)
img = Image.open(filepath)
if int(img.height) > 500 or int(img.width) > 500:
img.thumbnail(size, Image.ANTIALIAS)
file_path, file_name = os.path.split(filepath)
filepath = os.path.join(imgcheck_dir, file_name)
img.save(filepath)
file = {"media": open(filepath, "rb")}
res = requests.post(url=url, files=file)
try:
if int(res.json().get("errcode")) == 0:
return True, res.json().get("errmsg")
else:
return False, res.json().get("errmsg")
except:
return False, "檢驗圖片是否違規(guī)異常"
class Pay(WeiXinPay):
"""
付款到零錢
此處需做添加ip操作
1祷膳、登錄到微信支付商戶平臺
2陶衅、在產(chǎn)品中心找到企業(yè)付款到零錢
3、進入頁面之后直晨,找到產(chǎn)品配置按鈕搀军,點擊進入配置頁面膨俐。在"發(fā)起方式"的頁面下方點修改,添加發(fā)起支付的服務(wù)器外網(wǎng)IP
"""
def __init__(self, mch_appid, mchid, api_key):
super(Pay, self).__init__(mch_appid, mchid, api_key)
def post(self, openid, trade_no, amount, nonce_str, name="", ip=""):
kwargs = {
"openid": openid,
"partner_trade_no": trade_no,
"amount": amount, # 付款金額罩句,單位為分
"nonce_str": nonce_str
}
if ip:
kwargs["spbill_create_ip"] = ip
if name:
kwargs["re_user_name"] = name
self.update_params(kwargs)
return self.post_xml()[1]
class PayQuery(WeiXinPay):
"""
查詢企業(yè)付款
"""
def __init__(self, mch_appid, mchid, api_key):
super(PayQuery, self).__init__(mch_appid, mchid, api_key)
self.url = "https://api.mch.weixin.qq.com/mmpaymkttransfers/gettransferinfo"
def post(self, trade_no, nonce_str):
kwargs = {
"partner_trade_no": trade_no,
"nonce_str": nonce_str
}
self.update_params(kwargs)
return self.post_xml2()[1]
weixinPayV2 = Pay(WXPAY_APPID, WXPAY_MCHID, WXPAY_API_KEY)
相關(guān)鏈接
小程序支付: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/open/pay/chapter2_8_0.shtml
小程序獲取用戶openid:https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html
證書秘鑰使用說明:https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay3_0.shtml
簽名生成:https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay4_0.shtml
簽名驗證:https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay4_1.shtml
證書和回調(diào)報文解密;https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay4_2.shtml
付款到零錢:https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2
內(nèi)容安全(校驗一張圖片是否含有違法違規(guī)內(nèi)容):https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/sec-check/security.imgSecCheck.html