一.小程序登錄
登入 1小程序獲取code, 2 將code傳到后台,用code,appid,appseacert_id 換取openid,session_key 3 自定義登入方式,將key數據返回給小程序 4 如果我們的業務需要登入,那小程序就需要把key傳到后台
1. 登錄流程分析
小程序可以通過微信官方提供的登錄能力方便地獲取微信提供的用戶身份標識,快速建立小程序內的用戶體系
說明:
-
-
登錄憑證校驗接口:調用 code2Session 接口,換取 用戶唯一標識 OpenID 和 會話密鑰 session_key。
之后開發者服務器可以根據用戶標識來生成自定義登錄態,用於后續業務邏輯中前后端交互時識別用戶身份
注意:
-
會話密鑰
session_key
是對用戶數據進行 加密簽名 的密鑰。為了應用自身的數據安全,開發者服務器不應該把會話密鑰下發到小程序,也不應該對外提供這個密鑰,提供對應的key給前端,用的時候來后端對比查詢會話秘鑰 -
臨時登錄憑證 code 只能使用一次
流程總結:
第一步:小程序端執行wx.login后在回調函數中就能拿到上圖的code,然后把這個code傳給我們后端程序,
第二步:后端拿到這個這個code后,可以攜帶code和開發者信息,去請求code2Session接口拿到用的openid和session_key (openid是用戶在微信中唯一標識)
第三步:我們就可以把這個兩個值(val)存起來,然后返回一個鍵(key)給小程序端,下次小程序請求我們后端的時候,帶上這個key,我們就能找到這個val,就可以,這樣就把登入做好了
2.wx.login()和 code2Session 接口
1) wx.login() 接口(詳見官網)
小程序加載的時候就要登陸,因此登錄函數寫入app.json文件的onLaunch周期函數內部

//app.js App({ onLaunch: function () { var _this=this // 登錄 wx.login({ success: res => { console.log(res.code), // 發送 res.code 到后台換取 openId, sessionKey, unionId wx.request({ url: _this.globalData.Url+'/login/', data:{"code":res.code}, header:{"content-type":"application/json"}, method:"POST", success:function(res){ console.log(res) wx.setStorageSync("login_key", res.data.data.login_key) } }) } }) }, globalData: { Url:"http://127.0.0.1:8000", userInfo: null } })
2) code2Session (后端)
后端請求地址
GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
請求參數
請求返回值(json包)
登錄實例:
流程:寫路由 —— models建用戶表 —— 配置數據庫,緩存庫 —— 創建數據庫 —— 數據庫遷移 -——View中寫login(部分功能拆分到獨立文件)——傳給前端key前端保存

from django.conf.urls import url from django.contrib import admin from app01.view import User urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^login/',User.Login.as_view()), # url(r'^getinfo/',User.GetInfo.as_view()) ]

from django.db import models # Create your models here. class Wxuser(models.Model): #這些字段在授權里面可以看到,基本上都是從微信授權獲取 id = models.AutoField(primary_key=True) openid=models.CharField(max_length=255) # 這個是我們自己請求拿到的 name = models.CharField(max_length=50) avatar = models.CharField(max_length=200) language = models.CharField(max_length=50) province = models.CharField(max_length=50) city = models.CharField(max_length=50) country = models.CharField(max_length=50) #gender = models.CharField(max_length=50) creat_time = models.DateTimeField(auto_now_add=True) update_time = models.DateTimeField(auto_now=True) def __str__(self): # 后台管理中名字,不寫默認的全都是一個xxx return self.openid

INSTALLED_APPS = [ ...... 'app01.apps.App01Config', "rest_framework" ] DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'wx', 'USER':'root', 'PASSWORD':'123', 'HOST':'127.0.0.1', 'PORT': 3306, 'OPTIONS': {'charset': 'utf8mb4'}, } } # 注意字符,微信表情多就使用utf8mb4 CACHES = { 'default': { 'BACKEND': 'django_redis.cache.RedisCache', 'LOCATION': 'redis://127.0.0.1:6379', "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", "PASSWORD": "", }, }, }
為了方便管理我們建立views文件夾,分類寫各個視圖函數: (json包快捷解壓)
# views文件夾下user.py文件++++++++++++++++++++++++++++++++++++++++++++++++++++
from rest_framework.views import APIView from rest_framework.response import Response from app01.wx import wx_login from django.core.cache import cache import hashlib, time from app01 import models from app01.wx import WXBizDataCrypt from app01.my_ser import User_ser class Login(APIView): def post(self, request): # 前端請求了微信接口得到了code傳了過來 param = request.data code = param.get("code") if code: # 向登錄憑證校驗接口code2Session發送code,獲取返回的數據包 # (詳細參數組成見小程序登錄API) data = wx_login.login(code) if data: # 自定義登錄狀態,保存key=openid & session_key到緩存 # 返回給前端key,以后有業務請求攜帶key以示清白 val = data['openid'] + "&" + data["session_key"] key = data["openid"] + str(int(time.time())) # 保證key唯一 # 加密key,之后帶着密文過來,后端取出明文加密后和密文對比是否一致 md5 = hashlib.md5() md5.update(key.encode("utf-8")) key = md5.hexdigest() # 將key:val存入緩存庫redis,頻繁的業務請求不需要再請求數據庫 cache.set(key, val) print(key) # 查看用戶是否存在,存在就直接返回,不存在新增 has_user = models.Wxuser.objects.filter(openid=data['openid']).first() if not has_user: # 先存入openid,其他用戶信息在后面授權,從中微信中獲取 models.Wxuser.objects.create(openid=data['openid']) return Response({ "code": 200, "msg": "ok", "data": {"login_key": key} }) else: return Response({"code": 200, "msg": "code無效"}) else: return Response({"code": 200, "msg": "缺少參數"})
為了方便管理,我們將小功能拆分成文件不要都放在視圖中,所有微信相關東西放進新建的wx文件夾:
/wx/wx.login.py(發送請求模塊requests)

from app01.wx import settings import requests # 這個就是專門發連接請求的 def login(code): print('++++++++++',code) # 發送請求得到返回值為json包,下面即為向code2Session發送code(詳細參數見API) response=requests.get(settings.code2Session.format(settings.AppId,settings.AppSecret,code)) print(settings.AppId) print(settings.AppSecret) print(code) # 正常情況我們導入json模塊json.dump進行解壓 # 這里提供了快捷方法,如果是json數據,直接data.json()就可以解壓,不需要再導入模塊 data=response.json() print(data) if data.get("openid"): return data else: return False
/wx/settings.py

# 申請小程序后會給開發id和密碼 AppId="wxd..........329b58" AppSecret="a3082c926...........076c51c1fb88" # 官網提供,自己做了占位修改,方便之后填入數據 code2Session="https://api.weixin.qq.com/sns/jscode2session?appid={}&secret={}&js_code={}&grant_type=authorization_code" #下面兩個必須注冊公司之后才有 pay_mchid ='64.....405' # 工商號 pay_apikey = '8i3.......................d6j85' # 支付號
二、微信授權獲取用戶信息
授權 1 如果用戶已經授權,我們直接使用接口 2 如果沒有授權,就要吊起授權彈框,但是這個一個權限,是不能直接吊起彈框,這個接口是獲取用戶信息,我們 要讓小程序吊起彈框必須以按鈕的形式,吊起彈框 2.1 關於用戶信息,如果我們后台需要,則可以通過兩種方式,第一種是驗簽的形式,第二種是解密的形式, 2.2 但是這個兩種形式都要session_key,而且這個session_key是有時效性的 2.3 我們小程序可以通過wx.check_sesion_key判斷有沒有過期 2.4 如果過期,則要重新登入,獲取新的session_key
1.授權總覽
部分接口需要經過用戶授權同意才能調用。我們把這些接口按使用范圍分成多個 scope
,也就是授權分類的意思,例如用戶信息獲取:
用戶選擇對 scope
來進行授權,當授權給一個 scope
之后,其對應的所有接口都可以直接使用,scope詳細列表見官網
此類接口調用時:
- 如果用戶未接受或拒絕過此權限,會彈窗詢問用戶,用戶點擊同意后方可調用接口;
- 如果用戶已授權,可以直接調用接口;
- 如果用戶已拒絕授權,則不會出現彈窗,而是直接進入接口 fail 回調。(請開發者兼容用戶拒絕授權的場景,處理拒絕的情況,不要讓程序停在那里不走了)
1.1獲取用戶授權設置
開發者可以使用 wx.getSetting 獲取用戶當前的授權狀態。
1.2 打開設置界面(暫不了解)
用戶可以在小程序設置界面(「右上角」 - 「關於」 - 「右上角」 - 「設置」)中控制對該小程序的授權狀態。
開發者可以調用 wx.openSetting 打開設置界面,引導用戶開啟授權
1.3 提前發起授權請求
開發者可以使用 wx.authorize 在調用需授權 API 之前,提前向用戶發起授權請求
第一步:請求某個授權之前,我們可以先調用wx.getSetting查看是否已經授權,如果用戶之前已經同意授權,則不會出現彈窗,直接返回成功
第二步:如果還沒授權,調用wx.authorize接口申請授權
第三步:授權成功后我們再調取相關接口(scope列表里面有各個scope和對應接口)
2.一般授權
除了用戶信息授權,其他一般的授權都可以通過wx.authorize接口申請授權調起權限請求彈框
我們先做一個按鈕:
<button bind:tap="lu">錄音</button>
寫授權接口:

// test.js文件 lu:function(){ wx.getSetting({ success(res) { if (!res.authSetting['scope.record']) { wx.authorize({ scope: 'scope.record', success() { // 用戶已經同意小程序使用錄音功能,后續調用 wx.startRecord 接口不會彈窗詢問 wx.startRecord() } }) }else{ wx.startRecord() } } }) },
我們點擊按鈕,就會彈出是否授權彈窗
3. scope.userInfo 用戶信息授權
第一步:官方提示
wx.authorize({scope: "scope.userInfo"}),不會彈出授權窗口,請使用 <button open-type="getUserInfo"/>
也就是說用戶信息授權不能通過wx.authorize來請求,必須通過特定按鈕形式來申請
第二步:我們進入wx.getUserInfo :
wx.getUserInfo(Object object)請求成功后,object.success 回調函數參數詳解:
第三步:分析驗證和加密解密算法
1)驗簽
數據簽名校驗 為了確保開放接口返回用戶數據的安全性,微信會對明文數據進行簽名。開發者可以根據業務需要對數據包進行簽名校驗,確保數據的完整性 1.通過調用接口(如 wx.getUserInfo)獲取數據時,接口會同時返回 rawData、signature,其中 signature = sha1( rawData + session_key ) 2.開發者將 signature、rawData 和前端的session_key發送到開發者服務器進行校驗
3.服務器利用后端存儲的的 session_key 和rawData使用相同的算法計算出簽名 signature2 ,比對 signature 與 signature2 即可校驗數據的完整性
2) 解密
加密數據解密算法
接口如果涉及敏感數據(如wx.getUserInfo當中的 openId 和 unionId),接口的明文內容將不包含這些敏感數據。開發者如需要獲取敏感數據,
需要對接口返回的加密數據(encryptedData) 進行對稱解密
微信官方提供了四種編程語言的機密算法示例代碼(點擊下載)每種語言類型的接口名字均一致。 另外,為了應用能校驗數據的有效性,會在敏感數據加上數據水印( watermark )
3)會話密鑰 session_key 有效性 (坑點)
總結:別亂用登錄接口,會刷新session_key,我們可以通過wx.checkSession先檢查是否過期,再進行操作,的方法避免過期帶來的麻煩
開發者如果遇到因為 session_key 不正確而校驗簽名失敗或解密失敗,請關注下面幾個與 session_key 有關的注意事項: 1. 不要亂用wx.login ,調用時,用戶的 session_key 可能會被更新而致使舊 session_key 失效(刷新機制存在最短周期,如果同一個用戶短時間內多次調用 wx.login,
並非每次調用都導致 session_key 刷新)。開發者應該在明確需要重新登錄時才調用 wx.login,及時通過 auth.code2Session 接口更新服務器存儲的 session_key
2.session_key有效期不固定,微信會根據用戶使用小程序的行為對 session_key 進行續期,用戶越頻繁使用小程序,session_key 有效期越長
3.使用接口wx.checkSession可以校驗 session_key 是否有效,開發者在 session_key 失效時,可以通過重新執行登錄流程獲取有效的 session_key。
4.當開發者在實現自定義登錄態時,可以考慮以 session_key 有效期作為自身登錄態有效期,也可以實現自定義的時效性策略。
4.用戶信息獲取實例(解密方式)
第一步:前端
test.wxml 寫按鈕
<button open-type="getUserInfo" bindgetuserinfo="info">授權登錄</button>
test.js 校驗session_key ——請求信息接口——向后端發送請求(攜帶解密的參數)

info:function(res){ // console.log(res) wx.checkSession({ success() { //session_key 未過期,並且在本生命周期一直有效 wx.getUserInfo({ success:function(res){ // console.log(res) wx.request({ url: app.globalData.Url+"/getinfo/", data: {"encryptedData": res.encryptedData,"iv":res.iv,"login_key":wx.getStorageSync("login_key")}, method :"POST", header:{"content-type":"application/json"}, success:function(res){ console.log(res) } }) } }) }, fail() { // session_key 已經失效,需要重新執行登錄流程 wx.login() //重新登錄 } }) }
第二步:后端解密信息
路由、models、配置和登陸中一樣
views / user.py
class GetInfo(APIView): def post(self, request): param = request.data if param['encryptedData'] and param['iv'] and param['login_key']: # 緩存中取出我們在登陸中保存好的openid, seesion_key openid, seesion_key = cache.get(param['login_key']).split("&") # 官網下載的多語言解密包,取對應python的解密py文件,我們進行了封裝合並,傳入參數解密 data = WXBizDataCrypt.WXBizDataCrypt.getInfo(param['encryptedData'], param['iv'], seesion_key) save_data = { "name": data['nickName'], "avatar": data['avatarUrl'], "language": data['language'], "province": data['province'], "city": data['city'], "country": data['country'], } # 登陸中我們只保存了openid,這里將其余個人信息補全 models.Wxuser.objects.filter(openid=openid).update(**save_data) # 取出完整用戶信息,序列化后傳給前端 data = models.Wxuser.objects.filter(openid=openid).first() # 調用我們自定義的序列化函數 data = User_ser.User_ser(instance=data, many=False).data return Response({"code": 200, "msg": "缺少參數", "data": data}) else: return Response({"code": 200, "msg": "缺少參數"})
wx / WXBizDataCrypt.py 解密包2合1封裝版

import base64 import json from Crypto.Cipher import AES from app01.wx 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,encryptedData,iv,session_key): return cls(settings.AppId,session_key).decrypt(encryptedData, iv)
my_ser / User.ser 序列化文件
from rest_framework.serializers import ModelSerializer from app01 import models class User_ser(ModelSerializer): class Meta: model=models.Wxuser fields="__all__" # 所有字段,如果是部分序列化就用列表