1.REMOTE_ADDR:瀏覽當前頁面的用戶計算機的ip地址
2.HTTP_X_FORWARDED_FOR: 瀏覽當前頁面的用戶計算機的網關
1.小程序的登入
登入流程時序:
說明:
- 調用 wx.login() 獲取 臨時登錄憑證code ,並回傳到開發者服務器。
- 調用 auth.code2Session 接口,換取 用戶唯一標識 OpenID 和 會話密鑰 session_key。
之后開發者服務器可以根據用戶標識來生成自定義登錄態,用於后續業務邏輯中前后端交互時識別用戶身份。
注意:
- 會話密鑰
session_key
是對用戶數據進行 加密簽名 的密鑰。為了應用自身的數據安全,開發者服務器不應該把會話密鑰下發到小程序,也不應該對外提供這個密鑰。 - 臨時登錄憑證 code 只能使用一次
1)微信開發工具在加載app.js時,用onLaunch函數向微信服務器發送wx.login 獲取code;
2) 攜帶code向django后台發送 wx.request 發送請求,django后台拿到code,再加上AppId,AppSecret(在我們的微信開發平台申請的),向微信服務器提供的url鏈接使用requests模塊發送get請求
3)微信服務器返回的數據為json格式(.json()解析成字典),包括:openid,session_key,unionid,errcode,errmsg,根據返回的openid是否存在,檢驗請求微信服務器返回的結果是否成功;具體看開發平台文檔:https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html
將openid與時間戳的拼接的結果用MD5加鹽作為key,openid與session_key的拼接作為val,保存在redis緩存中;后期用戶登錄直接從緩存中的openid判斷用戶
4) 並根據openid(判別不同用戶的唯一標識)查詢數據庫是否存在,不存在就走create方法保存;
5)將這個key返回給前台,前台將openid緩存到本地,后期登錄時要攜帶上,參與用戶合法性校驗;wx.setStorageSync('login_key', res.data.data.login_key)
代碼:
app.js
//app.js
App({
onLaunch: function () { // 小程序的入口,加載小程序就會觸發這個函數
var _this=this
// 登錄
wx.login({ //調用微信服務器獲取code
success:res =>{
// console.log(res)
/*
code: "0613Wn2j1s2vct0Lzi2j1jZr2j13Wn2d"
errMsg: "login:ok"
*/
// 將code發送到后台換取openid、sessionKey、unionid
wx.request({
url: _this.globalData.Url + '/login/',
data :{'code':res.code},
method:'POST',
header:{'content-type':'application/json'},
success:function(res){
// console.log(res)
wx.setStorageSync('login_key', res.data.data.login_key) // 將login_key保存到本地緩存
}
})
}
})
},
globalData: {
Url:"http://127.0.0.1:8000", // 將后台的跟路由設置成全局變量,使用時就直接從這里拿
userInfo: null
}
})
view.py
import time,hashlib
from rest_framework.views import APIView
from rest_framework.response import Response
from django.core.cache import cache
from app01 import models
class LoginAPIView(APIView):
def post(self,request):
# print(request.data)
code = request.data.get('code')
if code:
data = wx_login.login(code)
if data:
openid = data.get('openid')
session_key = data.get('session_key')
val = openid + '&' + session_key
key = data.get('openid') + str(time.time())
md5 = hashlib.md5()
md5.update(key.encode('utf8'))
key = md5.hexdigest()
cache.set(key,val) # 保存到redis,在登錄授權獲取用戶信息的時候根據小程序提供的login_key,將value取出,用於用戶信息的解密。
has_user = models.Wxuser.objects.filter(openid=openid).first()
if not has_user:
models.Wxuser.objects.create(openid=openid)
return Response({
'code':0,
'msg': 'ok',
'data': {'login_key': key}
})
else:
return ({'code':1,'msg':'code無效'})
return Response({'code':1,'msg':'缺少參數'})
wx_login.py
import requests
from app01.utils import settings
def login(code):
# code2Session="https://api.weixin.qq.com/sns/jscode2session?appid={}&secret={}&js_code={}&grant_type=authorization_code" 開發文檔提供的微信服務器地址,向他發送請求獲取openid session_key
response = requests.get(settings.code2Session.format(settings.AppId, settings.AppSecret, code)) # openid session_key
data = response.json()
if data.get('openid'):
return data
2. 1用戶授權
# 所有的小程序的用戶的授權都可以通過wx.authorize授權,但是小程序用戶個人數據授權必須通過一個button按鈕進行授權,button按鈕中有幾個屬性值,看下面用戶數據授權
// 錄音授權
voice:function(){
// 可以通過 wx.getSetting 先查詢一下用戶是否授權了 "scope.record" 這個 scope
wx.getSetting({
success(res) {
if (!res.authSetting['scope.record']) {
wx.authorize({
scope: 'scope.record',
success() {
// 用戶已經同意小程序使用錄音功能,后續調用 wx.startRecord 接口不會彈窗詢問
wx.startRecord()
}
})
} else{
wx.startRecord()
}
}
})
}
2.2用戶數據授權接口
# wx.authorize({scope: "scope.userInfo"}),不會彈出授權窗口,請使用 <button open-type="getUserInfo"/>
# <!-- 需要使用 button 來授權登錄 -->
<button open-type="getUserInfo" bindgetuserinfo="info">授權登錄</button>
test.js
# const app = getApp()
page({
info:function(res){
// console.log(res) // encryptedData(包括敏感數據在內的完整用戶信息的加密數據)、errMsg("getUserInfo:ok")、iv(加密算法的初始向量)、rawData(不包括敏感信息的原始數據字符串,用於計算簽名)、signature(使用 sha1( rawData + sessionkey ) 得到字符串,用於校驗用戶信息)、userInfo(用戶信息對象,不包含 openid 等敏感信息)
wx.checkSession({ // 檢查用戶的ssession_key是否過期
success() {
//session_key 未過期,並且在本生命周期一直有效
wx.getUserInfo({
success:function(res){
// console.log(res)
wx.request({
url: app.globalData.Url+'/getuserinfo/',
data: {'encryptedData': res.encryptedData, 'iv': res.iv, 'login_key': wx.getStorageSync('login_key')}, // 獲取本地緩存的login_key,django后端從redis緩存中取出val(openid+session_key,) 這些參數都是后台調用解密算法獲取用戶信息的必要參數,還有后台存的AppId
method:"POST",
header:{'content_type':'application/json'},
success:function(res){
console.log(res)
}
})
}
})
},
fail() {
// session_key 已經失效,需要重新執行登錄流程
wx.login() //重新登錄
}
})
}
})
# console.log(res) 用戶信息
/*
data:
code: 0
msg: "ok"
user_data:
avatar: "https://wx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTJ3tS2CWSTzA2hsUibWhmXjHSvnyQp303JddtgSZBpdW1choVz4Wk0Whia87KJ8BgttzQrBRyYNtdFg/132"
city: "Fuyang"
country: "China"
creat_time: "2020-06-16T02:49:14.237796+08:00"
get_gender: "男"
language: "zh_CN"
name: "小抄"
province: "Anhui"
update_time: "2020-06-16T02:49:14.237796+08:00"
*/
views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from django.core.cache import cache
from app01.utils import wx_login,WXBizDataCrypt,settings
from app01 import models
from app01.serializer import user_ser
class UserInfoAPIView(APIView):
def post(self,request,):
# print(request.data)
data = request.data
login_key = data.get('login_key')
encryptedData = data.get('encryptedData')
iv = data.get('iv')
if login_key and encryptedData and iv:
openid,session_key = cache.get(login_key).split('&') # 從redis緩存中取出login_key(openid+session_key)
data = WXBizDataCrypt.WXBizDataCrypt.getinfo(session_key,iv,encryptedData) // 用戶信息解密
# print(data)
update_data = {
'name':data['nickName'],
'avatar': data['avatarUrl'],
'language': data['language'],
'province': data['province'],
'city': data['city'],
'country': data['country'],
'gender': data['gender'],
}
models.Wxuser.objects.filter(openid=openid).update(**update_data)
data = models.Wxuser.objects.filter(openid=openid).first()
user_data = user_ser.UserModelSerialezer(data,many=False).data
return Response({'code':0,'msg':'ok','user_data':user_data})
return Response({'code':1,'msg':'缺少參數'})
WXBizDataCrypt.py
去微信開發平台下載解密包,解密包需要Crypto模塊,自己pip安裝
# 下載解密包,封裝之后的代碼
import base64
import json
from Crypto.Cipher import AES
from app01.utils import settings
class WXBizDataCrypt:
def __init__(self, appId, sessionKey):
self.appId = appId
self.sessionKey = sessionKey
def decrypt(self, encryptedData, iv):
# base64 decode
sessionKey = base64.b64decode(self.sessionKey)
encryptedData = base64.b64decode(encryptedData)
iv = base64.b64decode(iv)
cipher = AES.new(sessionKey, AES.MODE_CBC, iv)
decrypted = json.loads(self._unpad(cipher.decrypt(encryptedData)))
if decrypted['watermark']['appid'] != self.appId:
raise Exception('Invalid Buffer')
return decrypted
def _unpad(self, s):
return s[:-ord(s[len(s)-1:])]
@classmethod
def getinfo(cls,sessionKey,iv,encryptedData):
appId = settings.AppId
return cls(appId, sessionKey).decrypt(encryptedData, iv)
3. 小程序支付
商戶系統和微信支付系統主要交互:
1小程序內調用登錄接口,獲取到用戶的openid,api參見公共api【小程序登錄API】
2、商戶server調用支付統一下單,api參見公共api【統一下單API】
商戶在小程序中先調用該接口在微信支付服務后台生成預支付交易單,返回正確的預支付交易后調起支付。
3、商戶server調用再次簽名,api參見公共api【再次簽名】
4、商戶server接收支付通知,api參見公共api【支付結果通知API】
5、商戶server查詢支付結果,api參見公共api【查詢訂單API】
# test.wxml
<button bind:tap="pay">支付</button>
# test.js
page({
pay:function(){
wx.request({ // 發起預支付
url: 'http://127.0.0.1:8000/pay/',
method: "POST",
data: { 'login_key': wx.getStorageSync('login_key')},
header: { 'content-type': 'application/json' },
success: function (e) { //在預支付的回調之后在發起支付
// console.log(e)
wx.requestPayment({
'timeStamp': e.data.data.timeStamp,
'nonceStr': e.data.data.nonceStr,
'package': e.data.data.package,
'signType': e.data.data.signType,
'paySign': e.data.data.paySign,
'success':function(res){
console.log(res,'成功') // {errMsg:'requestPayment:ok'} '成功'
},
'fail': function (res) { // 支付失敗 {errMsg:'requestPayment:fail cancel'} 用戶發起支付又退出窗口,導致支付失敗的回調接口
'支付失敗',res
},
})
}
})
}
})
url.py
urlpatterns = [
path('pay/', order.PayAPIView.as_view()),
]
order.py
import time,hashlib,random,requests
from rest_framework.views import APIView
from rest_framework.response import Response
from django.core.cache import cache
from app01.utils import settings
class PayAPIView(APIView):
def post(self, request):
param = request.data
if param.get('login_key'):
openid, session_key = cache.get(param.get("login_key")).split("&")
self.openid = openid
# 如果是Nginx做的負載就要HTTP_X_FORWARDED_FOR
if request.META.get('HTTP_X_FORWARDED_FOR'):
self.ip = request.META['HTTP_X_FORWARDED_FOR']
# 異步接收微信支付結果通知的回調地址,通知url必須為外網可訪問的url,不能攜帶參數
else:
# 如果沒有用Nginx就用REMOTE_ADDR
self.ip = request.META['REMOTE_ADDR']
data = self.pay()
return Response({"code": 0, "msg": "ok", "data": data})
return Response({"code": 1, "msg": "缺少參數"})
# nonce_str,主要保證簽名不可預測
def get_str(self):
str_all = "123456789abcdefghijkmnpqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ"
nonce_str = "".join(random.sample(str_all, 20))
return nonce_str
# 生成訂單號
def get_order(self):
out_trade_no = str(time.time())[-12:]
return out_trade_no
# 根據小程序的要求格式處理,並獲取簽名
def get_sign(self):
data_dic = {
"nonce_str": self.nonce_str,
"out_trade_no": self.out_trade_no,
"spbill_create_ip": self.ip,
"notify_url": self.notify_url,
"openid": self.openid,
"body": self.body,
"trade_type": "JSAPI",
"appid": self.appid,
"total_fee": self.total_fee,
"mch_id": self.mch_id
}
sign_str = "&".join([f"{k}={data_dic[k]}" for k in sorted(data_dic)]) # 按照key的從小到大順序排列並用&拼接
sign_str = f"{sign_str}&key={settings.pay_apikey}" # 微信給商戶的pay_apikey
# MD5加密並轉大寫
md5 = hashlib.md5()
md5.update(sign_str.encode("utf-8"))
return md5.hexdigest().upper()
# 將xml格式數據解析成字典格式
def xml_to_dict(self,data):
import xml.etree.ElementTree as ET
xml_dict = {}
data_dic = ET.fromstring(data)
for item in data_dic:
xml_dict[item.tag] = item.text
return xml_dict
# 二次簽名
def two_sign(self, prepay_id):
timeStamp = str(int(time.time()))
nonceStr = self.get_str()
data_dict = {
"appId": settings.AppId,
"timeStamp": timeStamp,
"nonceStr": nonceStr,
"package": f"prepay_id={prepay_id}",
"signType": "MD5"
}
sign_str = "&".join([f"{k}={data_dict[k]}" for k in sorted(data_dict)])
sign_str = f"{sign_str}&key={settings.pay_apikey}"
md5 = hashlib.md5()
md5.update(sign_str.encode("utf-8"))
sign = md5.hexdigest().upper()
data_dict["paySign"] = sign
data_dict.pop("appId")
return data_dict
def pay(self):
self.appid = settings.AppId
# 微信支付分配的商戶號,開發者才有
self.mch_id = settings.pay_mchid
self.nonce_str = self.get_str()
# nonce_str,主要保證簽名不可預測
self.body = "生活費"
# 訂單號
self.out_trade_no = self.get_order()
# 訂單總金額,單位為分
self.total_fee = 1
self.spbill_create_ip = self.ip
# 回調地址
self.notify_url = "http://www.baidu.com"
# 交易類型
self.trade_type = "JSAPI"
# 獲取簽名
self.sign = self.get_sign()
# 按照文檔要求拼接成項xml格式數據
data = f'''
<xml>
<appid>{self.appid}</appid>
<body>{ self.body}</body>
<mch_id>{self.mch_id}</mch_id>
<nonce_str>{self.nonce_str}</nonce_str>
<notify_url>{self.notify_url}</notify_url>
<openid>{self.openid}</openid>
<out_trade_no>{self.out_trade_no}</out_trade_no>
<spbill_create_ip>{self.spbill_create_ip}</spbill_create_ip>
<total_fee>{self.total_fee}</total_fee>
<trade_type>{self.trade_type}</trade_type>
<sign>{self.sign}</sign>
</xml>
'''
# 接口地址
url = "https://api.mch.weixin.qq.com/pay/unifiedorder"
response = requests.post(url, data.encode("utf-8"), headers={"content-type": "application/xml"})
# 返回的也是xml格式的數據,需要用xml模塊解析成字典
res_data = self.xml_to_dict(response.content)
# print(res_data)
# 二次簽名
data = self.two_sign(res_data["prepay_id"])
return data
4.模板消息
test.wxml
# report-submit是否返回 formId 用於發送模板消息
<form bindsubmit="form" report-submit="{{true}}">
<view class="section">
<view class="section__title">input</view>
<input name="input" placeholder="please input here" />
</view>
<view class="btn-area">
<button formType="submit">Submit</button>
<button formType="reset">Reset</button>
</view>
</form>
test.js
點擊提交按鈕觸發表單提交,將表單內的數據傳給了e
form:function(e){
console.log(e)
}