python项目中实现支付宝网页支付


支付流程

在一次项目中需要引入支付宝接口实现支付宝支付,使用场景如下:

  • 用户在我方商户系统中选择了购买商品,我方商户系统生成一张支付订单,用户点击订单的支付按钮后,页面会跳转到一个支付二维码的界面。
  • 用户使用手机支付宝扫码进行支付。
  • 支付完成后,显示支付成功或者失败,并在若干秒后返回已支付页面。

 

在支付宝的支付过程,会有三个主要的角色参与

  • 用户以及他使用的支付宝客户端
  • 我方的商户系统
  • 支付宝服务端

完整的支付流程如下:

  • 用户选择好商品下单,申城
  • 我方商户系统向支付宝提供用户选择商品的商户订单(必须有一个唯一的订单号),用户跳转到该订单的支付页面,并使用支付宝客户端进行扫码支付,支付宝从该用户账户中扣除对应的金额,表示支付宝服务端收到了用户的支付
  • 随后,支付宝服务端通知我方商户系统该订单支付成功,同时给商户系统的支付宝账号中增加对应的金额(实际会扣除部分手续费)。我方商户系统收到支付宝的消息,找到该订单的编号对应的订单,将其状态标记为已支付即可。

沙箱环境

简述

想要完成以上的支付流程,需要将我们在支付宝创建应用,并成为入驻服务商或者是商户,开启自己的商户号,用户收付款操作,而创建应用等操作需要经过官方的审核,并缴纳一定的保证金等,操作较为繁琐。

为了方便个人开发者测试,支付宝开发了一套沙箱环境,该环境和正式化境的实现基本相同,开发者用该环境进行支付,退款等等的测试内容项,测试完成后接入正式环境只需要配置相应的应用参数和密钥信息,并将接口指向正式环境即可。

创建沙箱环境

登录支付宝开放平台,进入沙箱环境展示页面。控制台 -> 研发服务 -> 砂箱环境页。

生成密钥

沙箱环境创建成功后,需要生成密钥和配置密钥,密钥的作用时实现商户系统和支付宝系统之间的数据传输不会被篡改。

  • 下载支付宝提供的密钥生成器。下载对应平台的工具,安装并进入生成工具。

  • 这里生成了应用公钥和应用私钥,并自动保存到了两个文件中。应用私钥属于商户系统私有,不能将其发送给任何人。而应用公钥信息我们需要提交给支付宝,在提交应用公钥后,支付宝服务端会将支付宝公钥返回给我们。

    • 点击上图中复制公钥
    • 然后在开发者平台提交给支付宝

    • 保存设置后,得到支付宝的公钥

 

  •  将这个支付宝公钥复制下来,保存到文件中,并和之前两个公钥信息存放在一起(可随意存放,存放在一起只是方便管理)。有了支付宝公钥,应用公钥,应用私钥即可完成数据加密。 

密钥的使用

至此:在我们的商户服务端保存了三个密钥文件。

  • 应用公钥
  • 应用私钥
  • 支付宝公钥

注:应用私钥和支付宝公钥要在签名使用还需要添加一个头尾字符串。

# 应用私钥
-----BEGIN RSA PRIVATE KEY-----

-----END RSA PRIVATE KEY-----

# 支付宝公钥
-----BEGIN PUBLIC KEY-----

-----END PUBLIC KEY-----

在支付宝服务端,我们将应用公钥进行了上传,这样支付宝服务端也有两个密钥:

  • 与我们的支付宝公钥对应的 支付宝私钥
  • 我们上传的 应用公钥

    在支付宝服务端,我们将应用公钥进行了上传,这样支付宝服务端也有两个密钥:

    • 与我们的支付宝公钥对应的 支付宝私钥
    • 我们上传的 应用公钥

 通过上面的流程描述,有两个重要的过程:

  • 我方商户系统需要向支付宝服务端发送订单信息,支付宝服务端用来生成用户支付的二维码。
  • 用户完成支付后,支付宝服务端需要通知我们的商户系统 xxx 编号的订单已经完成支付,并返回对应的支付信息等。

以上的两个步骤中,涉及到重要数据的网络传输,都可能会被他人截获数据,并可能更改相关的参数。支付宝保证安全的方式是在发送这个订单的相关信息时候,使用发送者自己的私钥计算得到一个加密的字符串,也就是其他人无法篡改也无法生成的签名,接收方使用私钥对应的公钥对这个签名进行验签。从而实现了双向数据传输的安全性,保证数据无法被篡改。

沙箱版支付宝客户端

使用沙箱环境进行测试,当然不能使用支付宝软件进行扫码付款,而是使用在开发者平台上下载沙箱版客户端,andriod手机安装完成后,需要使用开发者平台上提供的沙箱的账号进行登录,一个有两个账号,商家账号和买家账号,登录买家账号即可。登录完成后,后续可以使用该账户进行支付,使用方式和支付宝相同。

API接口

拥有上述的密钥,以及从开发者平台上获取的appid值等数据,就可以组织订单参数,向指定的api接口发送请求完成支付宝端功能的调用。在沙箱环境中,提供了以下的几个接口

接口英文名 接口中文名
alipay.trade.page.pay 统一收单下单并支付页面接口
alipay.trade.refund 统一收单交易退款接口
alipay.trade.fastpay.refund.query 统一收单交易退款查询接口
alipay.trade.query 统一收单线下交易查询接口
alipay.trade.close 统一收单交易关闭接口
alipay.data.dataservice.bill.downloadurl.query 查询对账单下载地址

使用不同的功能直接调用支付宝提供的不同接口即可,而在调用接口之前,需要准备必要的参数,以及完成数据加密得到数据的签名,只有数据完全按照支付宝规定的饿格式传输,在支付宝服务端,才能够对我们的数据验签成功,从而完成调用。

出上述接口外,还有众多其他的接口参数信息:https://opendocs.alipay.com/apis/api_1/alipay.trade.page.pay/?scene=API002020081300013629

统一下单接口参数

以支付接口为例,查看参数需要的参数列表。通常只需要关注一些重要的参数即可,下面列出部分参数,详情参考支付宝API参数文档:

公共请求参数

参数 类型 是否必填 最大长度 描述 示例值
app_id String 32 支付宝分配给开发者的应用ID 2014072300007148
method String 128 接口名称 alipay.trade.page.pay
return_url String 256 HTTP/HTTPS开头字符串 https://m.alipay.com/Gk8NF23
sign_type String 10 商户生成签名字符串所使用的签名算法类型,目前支持RSA2和RSA,推荐使用RSA2 RSA2
sign String 344 商户请求参数的签名串,详见签名 详见示例
timestamp String 19 发送请求的时间,格式"yyyy-MM-dd HH:mm:ss" 2014-07-24 03:07:50
version String 3 调用的接口版本,固定为:1.0 1.0
notify_url String 256 支付宝服务器主动通知商户服务器里指定的页面http/https路径。 http://api.test.alipay.net/atinterface/receive_notify.htm
biz_content String   请求参数的集合,最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递,具体参照各产品快速接入文档

biz_content = {

"subject": subject,

"out_trade_no": out_trade_no,

"total_amount": total_amount,

"product_code": "FAST_INSTANT_TRADE_PAY",

}

被指定的必须的参数,按照其说明在代码指定即可,然后对其进行签名即可。

签名与验签

  • 生成签名

当我们准备按参数后,便可以使用应用私钥对其加密,得到签名sign,将签名加入到请求参数中,请求api即可。

签名的简单步骤为

    • 将上述的需要的参数名和对应的值保存到一个字典中,并剔除sign这个键
    • 将这个字典按照key的进行排序,然后将字典序列化为url参数形式,即 `k1=v1&k2=v2&k3=v3`的形式
    • 对这个字符串进行Base64编码,然后使用私钥加密得到sign值,然后将sign值添加到序列化字符串之后。string + &sign=value
    • 请求参数完成,发送请求即可。
  • 验证签名

支付宝服务端向我们发送支付成功消息,其中包含了订单的基本信息,同样的,为了避免这些数据是没有被篡改的,我们就需要将明文数据使用公钥进行加密,得到签名,如果我们计算的到的签名和支付宝发送的签名相同,表示数据是安全,这个过程就被称为验证签名。

签名和验签代码

from datetime import datetime
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA256
from urllib.parse import quote_plus
from urllib.parse import urlparse, parse_qs
from base64 import decodebytes, encodebytes
import json


class AliPay(object):
    """
    支付宝支付接口(PC端支付接口)
    """

    def __init__(self, appid, app_notify_url, app_private_key_path,
                 alipay_public_key_path, return_url):
        self.appid = appid
        self.app_notify_url = app_notify_url
        self.app_private_key_path = app_private_key_path  # 密钥文件路径
        self.app_private_key = None
        self.return_url = return_url

        # 读取应用私钥和公钥
        with open(self.app_private_key_path) as fp:
            self.app_private_key = RSA.importKey(fp.read())
        self.alipay_public_key_path = alipay_public_key_path
        with open(self.alipay_public_key_path) as fp:
            self.alipay_public_key = RSA.importKey(fp.read())

    # 
    def direct_pay(self, subject, out_trade_no, total_amount, return_url=None, **kwargs):
        """
        传入 订单名,订单号,订单金额,跳转url 这些必要参数
        生成签名,并返回
        """
        biz_content = {
            "subject": subject,
            "out_trade_no": out_trade_no,
            "total_amount": total_amount,
            "product_code": "FAST_INSTANT_TRADE_PAY",
            # "qr_pay_mode":4
        }

        biz_content.update(kwargs)
        data = self.build_body("alipay.trade.page.pay", biz_content, self.return_url)
        return self.sign_data(data)

    
    def build_body(self, method, biz_content, return_url=None):
        """
        构建完整的参数,返回为签名的data参数字典。
        """
        data = {
            "app_id": self.appid,
            "method": method,
            "charset": "utf-8",
            "sign_type": "RSA2",
            "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            "version": "1.0",
            "biz_content": biz_content
        }

        if return_url is not None:
            data["notify_url"] = self.app_notify_url
            data["return_url"] = self.return_url

        return data

    def sign_data(self, data):
        """
        对data字典中的所有参数进行签名,得到sign签名,签名后的数据添加到原data字典中sign
        """
        data.pop("sign", None)
        # 排序后的字符串
        unsigned_items = self.ordered_data(data)
        unsigned_string = "&".join("{0}={1}".format(k, v) for k, v in unsigned_items)
        sign = self.sign(unsigned_string.encode("utf-8"))
        # ordered_items = self.ordered_data(data)
        quoted_string = "&".join("{0}={1}".format(k, quote_plus(v)) for k, v in unsigned_items)

        # 获得最终的订单信息字符串
        signed_string = quoted_string + "&sign=" + quote_plus(sign)
        return signed_string

    def ordered_data(self, data):
        complex_keys = []
        for key, value in data.items():
            if isinstance(value, dict):
                complex_keys.append(key)

        # 将字典类型的数据dump出来
        for key in complex_keys:
            data[key] = json.dumps(data[key], separators=(',', ':'))

        return sorted([(k, v) for k, v in data.items()])

    def sign(self, unsigned_string):
        # 开始计算签名:使用私钥加密得到sign签名。
        key = self.app_private_key
        signer = PKCS1_v1_5.new(key)
        signature = signer.sign(SHA256.new(unsigned_string))
        # base64 编码,转换为unicode表示并移除回车
        sign = encodebytes(signature).decode("utf8").replace("\n", "")
        return sign


    def _verify(self, raw_content, signature):
        # 开始计算签名
        key = self.alipay_public_key
        signer = PKCS1_v1_5.new(key)
        digest = SHA256.new()
        digest.update(raw_content.encode("utf8"))
        if signer.verify(digest, decodebytes(signature.encode("utf8"))):
            return True
        return False

    def verify(self, data, signature):
        """
        验证签名的方法,data为反序列化后的字典。 
        """
        # 从字典中获取签名类型
        if "sign_type" in data:
            sign_type = data.pop("sign_type")
        # 排序后的字符串
        unsigned_items = self.ordered_data(data)
        message = "&".join(u"{}={}".format(k, v) for k, v in unsigned_items)
        return self._verify(message, signature)

签名示例

if __name__ == "__main__":
    """支付请求过程"""
    # 传递参数初始化支付类
    alipay = AliPay(
        appid="2016080800192023",                                   # 设置签约的appid
        app_notify_url="http://projectsedus.com/",                  # 异步支付通知url
        app_private_key_path=u"ying_yong_si_yao.txt",               # 设置应用私钥
        alipay_public_key_path="zhi_fu_bao_gong_yao.txt",           # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
        debug=True,  # 默认False,                                   # 设置是否是沙箱环境,True是沙箱环境
        return_url="http://47.92.87.172:8000/"                      # 同步支付通知url
    )

    # 传递参数执行支付类里的direct_pay方法,返回签名后的支付参数,
    url = alipay.direct_pay(
        subject="测试订单",                              # 订单名称
        # 订单号生成,一般是当前时间(精确到秒)+用户ID+随机数
        out_trade_no="201702021225",                    # 订单号
        total_amount=100,                               # 支付金额
        return_url="http://47.92.87.172:8000/"          # 支付成功后,跳转url
    )
    
    # 将前面后的支付参数,拼接到支付网关
    # 注意:下面支付网关是沙箱环境,
    re_url = "https://openapi.alipaydev.com/gateway.do?{data}".format(data=url)
    print(re_url)
    # 最终进行签名后组合成支付宝的url请求

验签示例

if __name__ == "__main__":
    """支付宝支付成功后通知接口验证"""

    # 接收支付宝支付成功后,向我们设置的同步支付通知url,请求的参数
    return_url = 'http://47.92.87.172:8000/?total_amount=100.00×tamp=2017-10-11+22%3A44%3A17&sign=dHW%2F25EDd%2BYKqkU5krhseDNIOEyDpdJzSAaoqhTC0nlv8%2FEmrQVd0WqgGK0CS8Pax8sK4jIOdGLFa6lQEbIfzvH3Na2W949yCAYX04JL1Bi02wog7a8L7vfW9Kj%2BjfTQxumGH%2B1Drbezdg9gKOx3tX0cb1yBBdfifK6l1%2BE5UjggGbY60F6SD8A8XI06NMWb4ViU%2FLYtBhwAwU2koy1IK2%2BtBJM1xYFuBRlcWF61xCxexHwO0WEA3AwVRW1miuJjOpGiBTOwPI9Huj0WhkyRebIjBhSxReJdZIdTfAgwj4oqo4jAJCHDa6DKBM0H3wjKKXSyMeMBGKQB0Uv2rNdyng%3D%3D&trade_no=2017101121001004320200174640&sign_type=RSA2&auth_app_id=2016080800192023&charset=utf-8&seller_id=2088102170418468&method=alipay.trade.page.pay.return&app_id=2016080800192023&out_trade_no=201702021227&version=1.0'

    # 将同步支付通知url,传到urlparse
    o = urlparse(return_url)
    # 获取到URL的各种参数
    query = parse_qs(o.query)
    # 定义一个字典来存放,循环获取到的URL参数
    processed_query = {}
    # 将URL参数里的sign字段拿出来
    ali_sign = query.pop("sign")[0]

    # 传递参数初始化支付类
    alipay = AliPay(
        appid="2016080800192023",                                   # 设置签约的appid
        app_notify_url="http://projectsedus.com/",                  # 异步支付通知url
        app_private_key_path=u"ying_yong_si_yao.txt",               # 设置应用私钥
        alipay_public_key_path="zhi_fu_bao_gong_yao.txt",           # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
        debug=True,  # 默认False,                                   # 设置是否是沙箱环境,True是沙箱环境
        return_url="http://47.92.87.172:8000/"                      # 同步支付通知url
    )

    # 循环出URL里的参数
    for key, value in query.items():
        # 将循环到的参数,以键值对形式追加到processed_query字典
        processed_query[key] = value[0]
    # 将循环组合的参数字典,以及拿出来的sign字段,传进支付类里的verify方法,返回验证合法性,返回布尔值,True为合法,表示支付确实成功了,这就是验证是否是伪造支付成功请求
    print(alipay.verify(processed_query, ali_sign))

根据上述的方式就可以实现网站扫码支付,实现上述逻辑也比较繁琐,但支付宝官方只提供了java,php和.net的SDK包,没有提供python的版本的SDK,我们就只有手动实现以上逻辑。或者使用非官方版本的SDK。这是已给github的开源项目,实现了部分常用的API接口,这些接口的使用都可以使用该SDK完成。项目地址即文档说明:https://github.com/fzlee/alipay/blob/master/README.zh-hans.md,详细使用即可。

非官方SDK

安装

使用SDK可以实现支付宝相关接口的调用,按照接口的说明文档以及自己的需求,再指定的方法中传入指定的参数即可。这是SDK一个python库,可以直接使用pip进行安装,

# 安装python-alipay-sdk
pip install python-alipay-sdk --upgrade
# 对于python2, 请安装2.0以下版本: pip install python-alipay-sdk==1.1

使用教程

初始化

首先需要初始化一个SDK包中的实例对象,根据支付宝密钥的保存方式不同,可以使用两个类进行实例化, 上述使用的是复制支付宝公钥的方式保存支付宝公钥,所以使用 Alipay这个类即可。其他可以参考文档:https://github.com/fzlee/alipay/blob/master/README.zh-hans.md#初始化

初始化一个alipay实例

alipay = AliPay(
    appid="",             # appid
    app_notify_url=None,  # 默认回调url
    app_private_key_string=app_private_key_string,     # 应用私钥文件绝对路径
    # 支付宝的公钥文件路径,验证支付宝回传消息使用,不是你自己的公钥,
    alipay_public_key_string=alipay_public_key_string,
    sign_type="RSA2"     # RSA 或者 RSA2(默认), 与生成密钥时的密钥类型相同即可
    debug=False  # 默认False
)

调用接口

初始化对象后,调用该对象上相应的接口即可,该对象并没有实现支付宝API中的所有接口,如果缺少的接口就只有自己去实现。

对象的一个方法对应一个API接口,方法名可以由支付宝提供的接口名知道。将接口名中的点替换为下划线即为方法名,前面加上api_前缀即可。例如支付接口名alipay.trade.page.pay可以这么调用,

alipay.api_alipay_trade_page_pay(
    subject="测试订单",
    out_trade_no="2017020101",
    total_amount=100
)

调用对象的方法即可完成接口调用,接口中参数通过方法的参数指定接口。一些必要和常用参数会使用一个参数名字接收,其余不常用的参数,会被kwargs收集,一并作为接口参数传递。

不同的操作调用不同接口,文档中有各个接口的详细说明,如果是网站支付,如此调用即可:

subject = "测试订单"

# 电脑网站支付,需要跳转到https://openapi.alipay.com/gateway.do? + order_string
order_string = alipay.api_alipay_trade_page_pay(
    out_trade_no="20161112",
    total_amount=0.01,
    subject=subject,
    return_url="https://example.com",
    notify_url="https://example.com/notify" # 可选, 不填则使用默认notify url
)

url = "https://openapi.alipay.com/gateway.do?" + order_string

调用返回一个签名好的字符串,然后拼接为一个url,前端跳转到该url便可以使用自己支付宝扫码支付。