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)
}