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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM