python | 支付寶支付
一、支付寶支付
沙箱環境介紹
螞蟻沙箱環境(Beta)是協助開發者進行介面功能開發及主要功能聯調的輔助環境。沙箱環境模擬了開放平臺部分產品的主要功能和主要邏輯(當前沙箱支援產品請參考“沙箱支援產品列表”)。
在開發者應用上線稽核前,開發者可以根據自身需求,先在沙箱環境中瞭解、組合和除錯各種開放介面,進行開發調通工作,從而幫助開發者在應用上線稽核完成後,能更快速、更順利的進行線上除錯和驗收工作。
詳情,請參考連結:
https://docs.open.alipay.com/200/105311/
商家要使用支付寶支付功能,需要申請一個支付介面。需要提交企業的營業執照等相關資訊!
但是對於個人開發者來說,並沒有這些資料。所以,為了幫助開發者進行介面測試,個人可以使用沙箱環境測試。只要你有支付寶賬號就行。裡面提供了測試賬號,錢可以隨便充值。注意:資料是假的,金額可能會被清洗!
申請賬號
開啟連結
https://openhome.alipay.com/platform/appDaily.htm?tab=info
開啟手機支付寶,掃描二維碼,就可以登入了
登入成功之後,點選自研開發者
點選導航的開發者中心-->研發服務
填寫相關資訊,完成實名認證
再次進入沙箱應用,點選檢視應用公鑰
點選設定應用公鑰
點選檢視祕鑰生成方法
下載windows版本的壓縮包,解壓一下,效果如下:
點選RSA簽名驗籤工具,直接點選生成祕鑰
不要管JAVA和非JAVA,預設的就可以了
效果如下:
它會在當前壓縮包裡面的RSA祕鑰,生成2個txt檔案
開啟應用公鑰2048.txt,將裡面的內容複製一下
進入到剛才的頁面,輸入公鑰,點選儲存。
如果儲存時,提示公鑰格式不對。再生成一次公鑰,再次輸入,就可以了!
SDK
SDK的英文全名是:software development kit,翻譯成中文的意思就是“軟體開發工具包”
通俗一點的理解,是指由第三方服務商提供的實現軟體產品某項功能的工具包。一般以集合api和文件、範例、工具的形式出現。
通常SDK是由專業性質的公司提供專業服務的集合,比如提供安卓開發工具、或者基於硬體開發的服務等。也有針對某項軟體功能的SDK,如推送技術、影象識別技術、移動支付技術等,同時資源優勢類的公司也提供資源共享的SDK,如一些廣告SDK提供盈利渠道,分發SDK提供產品下載渠道。
注意:支付寶的SDK,採用了rsa加密演算法。對於傳送,返回的資料,它有一定的加密演算法以及解密演算法。
所以,我們要使用支付寶服務,傳送的資料,必須符合它的加密要求才行。有了SDK之後,我們不需要關心加密演算法,只需要關係業務邏輯就行!
開啟支付寶提供的SDK
https://docs.open.alipay.com/54/103419/
下載連結:
https://pypi.org/project/alipay-sdk-python/
不需要下載SDK!
不需要下載SDK!
不需要下載SDK!
這裡使用的是,根據官方SDK封裝好的一個python檔案,需要安裝依賴包pycryptodome
pip3 install pycryptodome
pay.py程式碼如下:
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, debug=False):
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())
if debug is True:
self.__gateway = "https://openapi.alipaydev.com/gateway.do"
else:
self.__gateway = "https://openapi.alipay.com/gateway.do"
def direct_pay(self, subject, out_trade_no, total_amount, return_url=None, **kwargs):
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 = {
"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.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):
# 開始計算簽名
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):
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)
這個pay.py就是sdk
支付寶金鑰處理體系
為了保證交易雙方(商戶和支付寶)的身份和資料安全,開發者在呼叫介面前,需要配置雙方金鑰,對交易資料進行雙方校驗。
整合和開發
在開始整合和開發前,首先了解一下常用的接入方式和架構建議:
其次,為了保證交易安全,支付寶採用了一系列的安全手段:
-
採用HTTPS協議傳輸交易資料,防止資料被截獲,解密。
-
採用RSA非對稱金鑰,明確交易雙方的身份,保證交易主體的正確性和唯一性
專案部署
新建一個django專案alipay,版本為django 2.x
在專案根目錄建立utils,將上面的pay.py複製到此目錄下
在專案根目錄建立keys
進入目錄secret_key_tools_RSA_win\RSA簽名驗籤工具windows_V1.4\RSA金鑰
將裡面的應用公鑰2048.txt,應用私鑰2048.txt這2個檔案,複製到keys目錄
python程式碼裡面的檔名,不要出現中文!
重新命名為英文!!!!!!!!
將這2個txt重新命名為alipay_public_2048.txt和app_private_2048.txt
注意:!!!!!!!!!!!!!!!!!!!!!!!!
alipay_public_2048.txt這個是公鑰,不能用本地的。要用網頁的!
點選檢視支付寶公鑰,不是應用公鑰!
複製裡面的公鑰
開啟alipay_public_2048.txt,必須增加頭部和尾部,將網頁複製的公鑰貼上到裡面!
-----BEGIN PUBLIC KEY----- MIIBI... -----END PUBLIC KEY-----
-----BEGIN PUBLIC KEY----- MIIBI... -----END PUBLIC KEY-----
效果如下:
一定要覆蓋公鑰!!!!!!!!!!!!!!!!!!!
開啟app_private_2048.txt,必須增加頭部和尾部
-----BEGIN RSA PRIVATE KEY----- MIIE... -----END RSA PRIVATE KEY-----
-----BEGIN RSA PRIVATE KEY----- MIIE... -----END RSA PRIVATE KEY-----
效果如下:
注意:這2個txt檔案,必須為utf-8編碼,否則啟動django專案會報錯!
修改urls.py,增加路徑
from django.contrib import admin
from django.urls import path,re_path
from app01 import views
urlpatterns = [
path('admin/', admin.site.urls),
path('', views.index),
path('index/', views.index),
re_path('buy/(?P<gid>\d+)/', views.buy),
path('check_order/', views.check_order),
re_path('show', views.show),
path('order_list/', views.order_list),
]
from django.contrib import admin from django.urls import path,re_path from app01 import views urlpatterns = [ path('admin/', admin.site.urls), path('', views.index), path('index/', views.index), re_path('buy/(?P<gid>\d+)/', views.buy), path('check_order/', views.check_order), re_path('show', views.show), path('order_list/', views.order_list), ]
修改models.py,增加2個表
from django.db import models
# Create your models here.
class Goods(models.Model): # 商品
name = models.CharField(max_length=32,verbose_name="名稱")
price = models.FloatField(verbose_name="價格")
class Order(models.Model): # 訂單
no = models.CharField(max_length=64,verbose_name="訂單號")
goods = models.ForeignKey(to='Goods',on_delete=models.CASCADE,verbose_name="商品ID")
status_choices = (
(1,'未支付'),
(2,'已支付'),
)
status = models.SmallIntegerField(choices=status_choices,default=1,verbose_name="支付狀態")
from django.db import models # Create your models here. class Goods(models.Model): # 商品 name = models.CharField(max_length=32,verbose_name="名稱") price = models.FloatField(verbose_name="價格") class Order(models.Model): # 訂單 no = models.CharField(max_length=64,verbose_name="訂單號") goods = models.ForeignKey(to='Goods',on_delete=models.CASCADE,verbose_name="商品ID") status_choices = ( (1,'未支付'), (2,'已支付'), ) status = models.SmallIntegerField(choices=status_choices,default=1,verbose_name="支付狀態")
修改views.py,增加檢視函式
注意:appid改成自己的,後6位我改成xx了
from django.shortcuts import render, HttpResponse, redirect
from app01 import models
import uuid
from utils.pay import AliPay # 匯入sdk
# Create your views here.
def index(request):
goods_list = models.Goods.objects.all()
return render(request, 'index.html', {'goods_list': goods_list})
# 全域性變數
alipay = AliPay(
appid="2016091700xxxxxx", # 注意,改成自己的!
app_notify_url="http://127.0.0.1:8000/check_order/", # POST,傳送支付狀態資訊
return_url="http://127.0.0.1:8000/show/", # GET,將使用者瀏覽器地址重定向回原網站
app_private_key_path="keys/app_private_2048.txt",
alipay_public_key_path="keys/alipay_public_2048.txt",
debug=True, # 預設True測試環境、False正式環境
)
def buy(request, gid):
"""
去購買並支付
:param request:
:param gid:
:return:
"""
obj = models.Goods.objects.get(pk=gid)
# 生成訂單(未支付)
no = str(uuid.uuid4())
models.Order.objects.create(no=no, goods_id=obj.id)
# 根據
# APPID
# 支付寶閘道器
# 公鑰和私鑰
# 生成要跳轉的地址
# 沙箱環境地址:https://openhome.alipay.com/platform/appDaily.htm?tab=info
query_params = alipay.direct_pay(
subject=obj.name, # 商品簡單描述
out_trade_no=no, # 商戶訂單號
total_amount=obj.price, # 交易金額(單位: 元 保留倆位小數)
)
pay_url = "https://openapi.alipaydev.com/gateway.do?{0}".format(query_params)
return redirect(pay_url)
def check_order(request):
"""
POST請求,支付寶通知支付資訊,我們修改訂單狀態
:param request:
:return:
"""
if request.method == 'POST':
from urllib.parse import parse_qs
body_str = request.body.decode('utf-8')
post_data = parse_qs(body_str)
post_dict = {}
for k, v in post_data.items():
post_dict[k] = v[0]
sign = post_dict.pop('sign', None)
status = alipay.verify(post_dict, sign)
if status:
# 支付成功,獲取訂單號將訂單狀態更新
out_trade_no = post_dict['out_trade_no']
models.Order.objects.filter(no=out_trade_no).update(status=2)
return HttpResponse('success')
else:
return HttpResponse('支援失敗')
else:
return HttpResponse('只支援POST請求')
def show(request):
"""
回到我們頁面
:param request:
:return:
"""
if request.method == "GET":
params = request.GET.dict()
sign = params.pop('sign', None)
status = alipay.verify(params, sign)
if status:
return HttpResponse('支付成功')
else:
return HttpResponse('失敗')
else:
return HttpResponse('只支援GET請求')
def order_list(request):
"""
檢視所有訂單狀態
:param request:
:return:
"""
orders = models.Order.objects.all()
return render(request, 'order_list.html', {'orders': orders})
from django.shortcuts import render, HttpResponse, redirect from app01 import models import uuid from utils.pay import AliPay # 匯入sdk # Create your views here. def index(request): goods_list = models.Goods.objects.all() return render(request, 'index.html', {'goods_list': goods_list}) # 全域性變數 alipay = AliPay( appid="2016091700xxxxxx", # 注意,改成自己的! app_notify_url="http://127.0.0.1:8000/check_order/", # POST,傳送支付狀態資訊 return_url="http://127.0.0.1:8000/show/", # GET,將使用者瀏覽器地址重定向回原網站 app_private_key_path="keys/app_private_2048.txt", alipay_public_key_path="keys/alipay_public_2048.txt", debug=True, # 預設True測試環境、False正式環境 ) def buy(request, gid): """ 去購買並支付 :param request: :param gid: :return: """ obj = models.Goods.objects.get(pk=gid) # 生成訂單(未支付) no = str(uuid.uuid4()) models.Order.objects.create(no=no, goods_id=obj.id) # 根據 # APPID # 支付寶閘道器 # 公鑰和私鑰 # 生成要跳轉的地址 # 沙箱環境地址:https://openhome.alipay.com/platform/appDaily.htm?tab=info query_params = alipay.direct_pay( subject=obj.name, # 商品簡單描述 out_trade_no=no, # 商戶訂單號 total_amount=obj.price, # 交易金額(單位: 元 保留倆位小數) ) pay_url = "https://openapi.alipaydev.com/gateway.do?{0}".format(query_params) return redirect(pay_url) def check_order(request): """ POST請求,支付寶通知支付資訊,我們修改訂單狀態 :param request: :return: """ if request.method == 'POST': from urllib.parse import parse_qs body_str = request.body.decode('utf-8') post_data = parse_qs(body_str) post_dict = {} for k, v in post_data.items(): post_dict[k] = v[0] sign = post_dict.pop('sign', None) status = alipay.verify(post_dict, sign) if status: # 支付成功,獲取訂單號將訂單狀態更新 out_trade_no = post_dict['out_trade_no'] models.Order.objects.filter(no=out_trade_no).update(status=2) return HttpResponse('success') else: return HttpResponse('支援失敗') else: return HttpResponse('只支援POST請求') def show(request): """ 回到我們頁面 :param request: :return: """ if request.method == "GET": params = request.GET.dict() sign = params.pop('sign', None) status = alipay.verify(params, sign) if status: return HttpResponse('支付成功') else: return HttpResponse('失敗') else: return HttpResponse('只支援GET請求') def order_list(request): """ 檢視所有訂單狀態 :param request: :return: """ orders = models.Order.objects.all() return render(request, 'order_list.html', {'orders': orders})
增加index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<ul>
{% for row in goods_list %}
<li>{{ row.name }},價格:{{ row.price }} <a href="/buy/{{ row.id }}/">購買</a></li>
{% endfor %}
</ul>
</body>
</html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <ul> {% for row in goods_list %} <li>{{ row.name }},價格:{{ row.price }} <a href="/buy/{{ row.id }}/">購買</a></li> {% endfor %} </ul> </body> </html>
增加order_list.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<table border="1">
{% for item in orders %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.no }}</td>
<td>{{ item.goods.name }}</td>
<td>{{ item.get_status_display }}</td>
</tr>
{% endfor %}
</table>
</body>
</html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <table border="1"> {% for item in orders %} <tr> <td>{{ item.id }}</td> <td>{{ item.no }}</td> <td>{{ item.goods.name }}</td> <td>{{ item.get_status_display }}</td> </tr> {% endfor %} </table> </body> </html>
修改settings.py,關閉csrf
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
# 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', # 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ]
專案結構如下:
alipay/
├── alipay
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── app01
│ ├── admin.py
│ ├── apps.py
│ ├── __init__.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── 0002_auto_20180810_1623.py
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── db.sqlite3
├── keys
│ ├── alipay_public_2048.txt
│ └── app_private_2048.txt
├── manage.py
├── templates
│ ├── index.html
│ └── order_list.html
└── utils
└── pay.py
alipay/ ├── alipay │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── app01 │ ├── admin.py │ ├── apps.py │ ├── __init__.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20180810_1623.py │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ └── views.py ├── db.sqlite3 ├── keys │ ├── alipay_public_2048.txt │ └── app_private_2048.txt ├── manage.py ├── templates │ ├── index.html │ └── order_list.html └── utils └── pay.py
使用2個命令,生成表
python manage.py makemigrations python manage.py migrate
使用navicat開啟sqlite3資料庫,增加幾條資料
INSERT INTO app01_goods ("id", "name", "price") VALUES (1, 'Apple iPad Pro', 5588.0); INSERT INTO app01_goods ("id", "name", "price") VALUES (2, 'i5 8500', 1599.0); INSERT INTO app01_goods ("id", "name", "price") VALUES (3, '拯救者Y7000', 7299.0); INSERT INTO app01_goods ("id", "name", "price") VALUES (4, '大白兔奶糖', 1.0);
INSERT INTO app01_goods ("id", "name", "price") VALUES (1, 'Apple iPad Pro', 5588.0); INSERT INTO app01_goods ("id", "name", "price") VALUES (2, 'i5 8500', 1599.0); INSERT INTO app01_goods ("id", "name", "price") VALUES (3, '拯救者Y7000', 7299.0); INSERT INTO app01_goods ("id", "name", "price") VALUES (4, '大白兔奶糖', 1.0);
啟動django專案,訪問首頁
本地測試支付
點選購買大白兔奶糖,它會自動跳轉到支付寶的支付頁面
注意:要沙箱環境的APP
它只提供了安卓版本,下載之後,進行安裝
點選沙箱賬號
它提供了商家資訊和買家資訊
預設的買家資訊,賬號有9萬多塊。注意:這個錢是虛擬的,不能體現!
如果錢不夠了,可以輸入任意金額進行充值!
手機登入買家賬號,密碼為111111
開啟掃一掃,進行支付
提示支付成功
注意:如果出現失敗,可能是公鑰錯了!一定是網頁的支付寶公鑰,不是本地的公鑰!
檢視買家資訊,少了一塊錢
檢視商家資訊,多了一塊錢。那一分錢,是扣了手續費!
注意:這個是本地環境測試的。
如果需要伺服器測試,請修改views.py中的全域性變數,將127.0.0.1改為伺服器的公網IP即可!
檢視訂單狀態,這裡的狀態是未支付。為什麼呢?因為支付寶要傳送一個POST請求到
http://127.0.0.1:8000/check_order/ 才能修改狀態。由於在本地,支付寶無法訪問此地址!
注意:商品價格,必須保證最多為小數點2位。這個是支付寶規定的!
線上測試支付
將程式碼上傳到公網伺服器,修改views.py,將裡面的127.0.0.1改成公網IP。
再次支付一次,檢視訂單狀態。狀態為已支付
完整程式碼,請引數github
https://github.com/987334176/alipay
總結:
1. 什麼是SDK? - 第三方服務商提供的實現軟體產品某項功能的工具包 2. 沙箱環境是什麼? - 是測試環境 3. 簡述支付寶支付流程: - 使用者點選支援,根據支付寶的AppID/閘道器/公鑰私鑰/SDK生成地址 - 使用者跳轉到支付寶 - 支付成功之後,支付寶會給我傳送兩個請求: - GET, 從支付寶網站跳轉回自己網站。 - POST,支付傳送支付資訊(修改訂單狀態) 4. 支付寶金額的精度? - 小數點後兩位 5. 支付寶用的什麼加密? - RSA 6. 支付完成之後,伺服器宕機了,怎麼辦? - 24小時內,支付依然傳送通知。