1. 程式人生 > 實用技巧 >python專案中實現支付寶網頁支付

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&timestamp=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便可以使用自己支付寶掃碼支付。