使用蚂蚁金服开放平台的沙箱环境
第一步:获取公钥、私钥
首先,进入蚂蚁金服开放平台官方主页, 点击文档中心的开发文档,往下翻,找到开发工具-沙箱环境
进入沙箱环境页面,系统已经自动为你创建一个应用,在基础信息中可以看到应用信息。
这里RSA2公钥我已经生成了,新用户可点击开发文档/ 签名专区 / 生成RSA密钥
查看帮助文档来生成
支付宝提供一键生成工具便于开发者生成一对RSA密钥,可通过下方链接下载密钥生成工具:
WINDOWS
MAC_OSX
下载该工具后,解压打开文件夹,运行“RSA签名验签工具.bat”(WINDOWS)或“RSA签名验签工具.command”(MAC_OSX)。
界面示例如下,注意python语言密钥格式选择PKCS1(非JAVA适用),密钥长度选择更安全的2048
将私钥和公钥都复制到项目应用里存放,存为txt文件即可,文本内容在首尾分别加上“-----BEGIN PRIVATE KEY-----”和“-----END PRIVATE KEY-----”
-----BEGIN PRIVATE KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwlSXWV1bGwcZf720nUMskUGoWbyPIXm/P+yvPExy1SQHltmFncuYNB53Pk/kWiO3iP4aHgDfh3Dkw9JbLqrpNzM5ch/QfEIAY7jy15yPg81DrBNP0Sh+AbPzjMB/TyO/tIc5kzBhDZ9jXes6VybvYppAlYxzILWmA5nM5dPHQUn1Wpm7oQbuLUy1EKP0aW3R/lFIWzLuSAQvh4EExJXKGvH1M65TN5gwBFibSVpwo6YVsYJfx3zhR6JbO3bc5er1xzjHo0jffQ7ujE6Gys5VV2k6F2Spu6nAshBrxP9L+kI9aO4zKMAulthbFueHkaJGowzboNk1IE3uKnvxHN6CiQIDAQAB
-----END PRIVATE KEY-----
支付宝的公钥在沙箱环境获得,也一并存到项目下以后验证时要用
第二步:查看API文档
1.首先还是进入蚂蚁金服开放平台官方主页
点击文档中心的开发文档,往下翻,找到左侧产品文档的电脑网站支付,选择API列表,进入统一收单下单支付页面接口查看文档
网关
公共参数,重点关注以下字段
- appid :2016091700535495(填写沙箱应用的appid)
- return_url: 对于PC网站支付的交易,在用户支付完成之后,支付宝会根据API中商户传入的return_url参数,通过GET请求的形式将部分支付结果参数通知到商户系统, 即同步返回地址,即支付成功跳转的页面url。同步通知详情
- sign 签名字段,最重要的字段,后面会讲,详见签名
- notify_url: 对于PC网站支付的交易,在用户支付完成之后,支付宝会根据API中商户传入的notify_url,通过POST请求的形式将支付结果作为参数通知到商户系统。异步通知详情
- biz_content:除公共参数以外的其他必要参数
请求参数这些字段是最后放入公共字段里的biz_content,着重关注必填项
第三步:签名与验签
签名由于SDK没有python,所以自行实现签名,点击下方此处流程
1.筛选并排序
获取所有请求参数,不包括字节类型参数,如文件、字节流,剔除sign
字段,剔除值为空的参数,并按照第一个字符的键值ASCII码递增排序(字母升序排序),如果遇到相同字符则按照第二个字符的键值ASCII码递增排序,以此类推。
2.拼接
将排序后的参数与其对应值,组合成“参数=参数值”的格式,并且把这些参数用&
字符连接起来,此时生成的字符串为待签名字符串。
例如下面的请求示例,参数值都是示例,开发者参考格式即可:
REQUEST URL: https://openapi.alipay.com/gateway.do
REQUEST METHOD: POST
CONTENT:
app_id=2014072300007148
method=alipay.mobile.public.menu.add
charset=GBK
sign_type=RSA2
timestamp=2014-07-24 03:07:50
biz_content={"button":[{"actionParam":"ZFB_HFCZ","actionType":"out","name":"话费充值"},{"name":"查询","subButton":[{"actionParam":"ZFB_YECX","actionType":"out","name":"余额查询"},{"actionParam":"ZFB_LLCX","actionType":"out","name":"流量查询"},{"actionParam":"ZFB_HFCX","actionType":"out","name":"话费查询"}]},{"actionParam":"http://m.alipay.com","actionType":"link","name":"最新优惠"}]}
sign=e9zEAe4TTQ4LPLQvETPoLGXTiURcxiAKfMVQ6Hrrsx2hmyIEGvSfAQzbLxHrhyZ48wOJXTsD4FPnt+YGdK57+fP1BCbf9rIVycfjhYCqlFhbTu9pFnZgT55W+xbAFb9y7vL0MyAxwXUXvZtQVqEwW7pURtKilbcBTEW7TAxzgro=
version=1.0
则待签名字符串为下图:可以看出字段进行了排序并且完成了拼接
app_id=2014072300007148&biz_content={"button":[{"actionParam":"ZFB_HFCZ","actionType":"out","name":"话费充值"},{"name":"查询","subButton":[{"actionParam":"ZFB_YECX","actionType":"out","name":"余额查询"},{"actionParam":"ZFB_LLCX","actionType":"out","name":"流量查询"},{"actionParam":"ZFB_HFCX","actionType":"out","name":"话费查询"}]},{"actionParam":"http://m.alipay.com","actionType":"link","name":"最新优惠"}]}&charset=GBK&method=alipay.mobile.public.menu.add&sign_type=RSA2×tamp=2014-07-24 03:07:50&version=1.0
3.调用签名函数
使用各自语言对应的SHA256WithRSA(对应sign_type为RSA2)或SHA1WithRSA(对应sign_type为RSA)签名函数利用商户私钥对待签名字符串进行签名,并进行Base64编码。
4.把生成的签名赋值给sign参数,拼接到请求参数中。
验签
1.在通知返回参数列表中,除去
sign
、sign_type
两个参数外,凡是通知返回回来的参数皆是待验签的参数。
2.将剩下参数进行url_decode, 然后进行字典排序,组成字符串,得到待签名字符串:
3.将签名参数(
sign
)使用base64解码为字节码串。
4.使用
RSA2
的验签方法,通过签名字符串、签名参数(经过base64解码)及支付宝公钥验证签名,根据返回结果判定是否验签通过。
代码段
import json
from datetime import datetime
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA256
from base64 import b64encode, b64decode
from urllib.parse import urlparse, parse_qs, quote_plus
class Alipay(object):
def __init__(self, app_id, app_private_key_path, alipay_public_key_path, debug=True, return_url=None, notify_url=None):
self.app_id = app_id
self.debug = debug
self.return_url = return_url
self.notify_url = notify_url
with open(app_private_key_path) as fp:
self.app_private_key = RSA.importKey(fp.read())
with open(alipay_public_key_path) as fp:
self.alipay_public_key = RSA.importKey(fp.read())
if debug:
self.__gateway = "https://openapi.alipaydev.com/gateway.do"
else:
self.__gateway = "https://openapi.alipay.com/gateway.do"
def build_url(self, biz_content):
"""
:param biz_content: 请求参数部分
:return: 完整的访问地址url字符串
"""
# 初始化必填公共字段,sign除外
data = {
"app_id": self.app_id,
"method": "alipay.trade.page.pay",
"charset": "utf-8",
"sign_type": "RSA2",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"version": "1.0",
"biz_content": biz_content
}
if self.notify_url:
data["notify_url"] = self.notify_url
if self.return_url:
data["return_url"] = self.return_url
# 对待签名字符串进行RSA签名
sign_str = self.get_request_url_str(data, flag=False)
sign = self.rsa_sign(sign_str.encode("utf-8"))
# 将签好名的sign字段拼接到请求字符串最后,字符串需对斜线进行编码
request_url_str = self.get_request_url_str(data)
request_url_str += "&sign={}".format(quote_plus(sign))
print(self.__gateway + '?' + request_url_str)
return self.__gateway + '?' + request_url_str
def translate_url(self, returl_url):
"""
:param returl_url:返回字符串
:return: 解析出请求参数字符串及签名
"""
o = urlparse(return_url)
query = parse_qs(o.query)
processed_query = {}
ali_sign = query.pop("sign")[0] if "sign" in query else None
if "sign_type" in query:
sign_type = query.pop("sign_type")[0]
for key, value in query.items():
processed_query[key] = value[0]
request_url_str = self.get_request_url_str(processed_query, flag=False).encode("utf-8")
# 将解析后的request_url_str字符串和签名参数拼接成元祖
data = ([request_url_str, ali_sign])
return data
def get_biz_content(self, out_trade_no, total_amount, subject, **kwargs):
"""
获取请求参数
"""
biz_content = {
"out_trade_no": out_trade_no,
"product_code": "FAST_INSTANT_TRADE_PAY",
"total_amount": total_amount,
"subject": subject,
}
biz_content.update(kwargs)
return biz_content
def get_request_url_str(self, data, flag=True):
"""
:param data: 获取的字典型请求参数
:param flag: 是否对字典的值进行编码斜线
:return:排好序并用&进行拼接的url字符串
"""
#对键值为字典的转化为str
for k, v in data.items():
if isinstance(v, dict):
data[k] = json.dumps(v)
# 按key值字母升序排序为列表,方便拼接
data_lst = sorted([(k, v) for k, v in data.items()])
if flag:
request_url_str = "&".join(["{k}={v}".format(k=k, v=quote_plus(v)) for k, v in data_lst])
else:
request_url_str = "&".join(["{k}={v}".format(k=k, v=v) for k, v in data_lst])
return request_url_str
def rsa_sign(self, data):
"""
利用商户私钥对待签名字符串进行签名
:param data: 待签名data,格式bytes
:return: 生成的签名字符串
"""
private_key = self.app_private_key
hash_obj = SHA256.new(data)
signer = PKCS1_v1_5.new(private_key)
signature = signer.sign(hash_obj)
sign = b64encode(signature).decode("utf-8")
return sign
def rsa_verify(self, raw_data, signature):
"""
使用RSA2的验签方法,通过签名字符串、签名参数(经过base64解码)及支付宝公钥验证签名,
根据返回结果判定是否验签通过
:param raw_data: 原签名字符串data,格式bytes
:param signature: 签名参数
:return: 验签是否通过
"""
public_key = self.alipay_public_key
hash_obj = SHA256.new(raw_data)
verifier = PKCS1_v1_5.new(public_key)
return verifier.verify(hash_obj, b64decode(signature.encode("utf-8")))
if __name__ == "__main__":
alipay = Alipay(
app_id="2016091700535495",
return_url="http://101.200.32.20:8000/",
notify_url="http://projectsedus.com/",
app_private_key_path="../trade/keys/private_2048.txt",
alipay_public_key_path="../trade/keys/alipay_keys_2048.txt",
)
content = alipay.get_biz_content(out_trade_no="201802021232", total_amount=1, subject="测试支付")
alipay.build_url(content)
# 根据return_url获取请求参数
return_url = "http://101.200.32.20:8000/?charset=utf-8&out_trade_no=201702021230&method=alipay.trade.page.pay.return&total_amount=1.00&sign=gvXy%2FXOSOUQupNTn%2Fi%2BdkpqqcDc0Ia8R5lZ2kQRl5UcVb6w83pJqadDPzjdCTLq7%2F4aoIEHhAR6%2FOZwXmsEGWGd2DNynjiGF2mzIpKjDIaGLhdlP9DMIVy%2BIuCfLB7tHC9n1%2BPYoH1YYVWIqO%2B5FLcTh77ucwP66Glq5gFBKpD0b6fiPYlAjxO%2FAIEiyRxSaZS3fj%2BBOhB%2Fo31rjMLa5EOHgvdDeP8OTa01VVJ3wAMBDHyGslj5cShZQwdALyuxjGTFOb0PHFiWEjS828j7ZiTkWiODZ2QFClLE9NHwR6VrlnT0Ttss9Hfu8zwIBMsK3SWt9f32TnmC2wq7hkvMsyg%3D%3D&trade_no=2018082021001004950200556565&auth_app_id=2016091700535495&version=1.0&app_id=2016091700535495&sign_type=RSA2&seller_id=2088102176097894×tamp=2018-08-20+23%3A23%3A12"
data = alipay.translate_url(return_url)
print(alipay.rsa_verify(data[0], data[1]))