官方文檔:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
小程序登錄
小程序可以通過微信官方提供的登錄能力方便地獲取微信提供的用戶身份標識,快速建立小程序內的用戶體系。
登錄流程
說明:
調用 wx.login() 獲取 臨時登錄憑證code ,並回傳到開發者服務器。
調用 auth.code2Session 接口,換取 用戶唯一標識 OpenID 和 會話密鑰 session_key。
之后開發者服務器可以根據用戶標識來生成自定義登錄態,用於后續業務邏輯中前后端交互時識別用戶身份。
注意:
會話密鑰 session_key 是對用戶數據進行 加密簽名 的密鑰。為了應用自身的數據安全,開發者服務器不應該把會話密鑰下發到小程序,也不應該對外提供這個密鑰。
臨時登錄憑證 code 只能使用一次
小程序端執行wx.login后在回調函數中就能拿到上圖的code,然后把這個code傳給我們后端程序,后端拿到這個這個code后,可以請求code2Session接口拿到用的openid和session_key,openid是用戶在微信中唯一標識,我們就可以把這個兩個值(val)存起來,然后返回一個鍵(key)給小程序端,下次小程序請求我們后端的時候,帶上這個key,我們就能找到這個val,就可以,這樣就把登入做好了。
總結:小程序中執行wx.login,獲取code,在后端請求code2Session接口,傳入code參數,拿到openid和session_key。
1.wx.login() (該函數在app.js中)
調用接口獲取登錄憑證(code)。通過憑證進而換取用戶登錄態信息,包括用戶的唯一標識(openid)及本次登錄的會話密鑰(session_key)等。用戶數據的加解密通訊需要依賴會話密鑰完成。更多使用方法詳見 小程序登錄。
參數
object.success 回調函數
參數
Object res
示例代碼 (獲取code值傳遞到后端)
var _this=this
wx.login({ success: res => { // 發送 res.code 到后台換取 openId, sessionKey, unionId wx.request({ url: _this.globalData.Url+'/login/', #后台接口 data:{"code":res.code}, #傳遞給后台使用的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
登錄憑證校驗。通過 wx.login 接口獲得臨時登錄憑證 code 后傳到開發者服務器調用此接口完成登錄流程。更多使用方法詳見 小程序登錄。
請求地址
GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
請求參數
返回值
errcode 的合法值
示例代碼
settings.py (配置請求代碼)
AppId="wx29fad388b1f51644" AppSecret="d00c23ad3faf96ca218c20f6aaece7a7" code2Session="https://api.weixin.qq.com/sns/jscode2session?appid={}&secret={}&js_code={}&grant_type=authorization_code" pay_mchid ='1415981402' pay_apikey = 'xi34nu5jn7x2uujd8u4jiijd2u5d6j8e'
wx_login.py (傳入小程序獲得的code,請求code2Session,得到openid和session_key)
from app01.wx import settings import requests def login(code): response=requests.get(settings.code2Session.format(settings.AppId,settings.AppSecret,code)) data=response.json() if data.get("openid"): return data else: return False
models.py
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): return self.openid
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): param=request.data #獲取小程序傳遞過來的參數 if param.get("code"): data=wx_login.login(param.get("code")) #小程序傳遞過來的code放入codeSession請求,來獲得openid和session_key if data: val=data['openid']+"&"+data["session_key"] #獲取到openid和session_key做一下拼接 key=data["openid"]+str(int(time.time())) #生成一個key傳給小程序 md5=hashlib.md5() md5.update(key.encode("utf-8")) key=md5.hexdigest() #加密 cache.set(key,val) #存入后端的redis中 has_user=models.Wxuser.objects.filter(openid=data['openid']).first() #判斷數據庫中不存在該openid就保存在數據庫中 if not has_user: models.Wxuser.objects.create(openid=data['openid']) #存在后端數據庫中 return Response({ "code":200, "msg":"ok", "data":{"login_key":key} #傳遞給小程序數據key,下次小程序請求拿key取值 }) else: return Response({"code": 200, "msg": "code無效"}) else: return Response({"code":200,"msg":"缺少參數"})
至此,微信登錄就算完成了,但是上面提及的session_key還沒用到。這個會在下面用戶授權使用到,用來解密用戶的數據。
微信授權獲取用戶
后端獲取微信用戶信息流程
因為session_key會過期,所以我們使用之前先用checksession先檢測一下是否過期,如果過期了再重新生成。
官方文檔:https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/wx.checkSession.html
1.wx.checkSession
檢查登錄態是否過期。
通過 wx.login 接口獲得的用戶登錄態擁有一定的時效性。用戶越久未使用小程序,用戶登錄態越有可能失效。反之如果用戶一直在使用小程序,則用戶登錄態一直保持有效。具體時效邏輯由微信維護,對開發者透明。開發者只需要調用 wx.checkSession 接口檢測當前用戶登錄態是否有效。
登錄態過期后開發者可以再調用 wx.login 獲取新的用戶登錄態。調用成功說明當前 session_key 未過期,調用失敗說明 session_key 已過期。更多使用方法詳見 小程序登錄。
參數
示例代碼
test.wxml
<button open-type="getUserInfo" bindgetuserinfo="info">授權登錄</button>
test.js
info:function(res){ wx.checkSession({ #檢測session_key是否過期 success() { //session_key 未過期,並且在本生命周期一直有效 wx.getUserInfo({ #沒有過期,可以查詢用戶的個人信息 success:function(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() //重新登錄 } }) }
2.wx.getSetting() 獲取用戶當前的授權狀態
當你想獲取用戶的信息,或者想讓用戶做一些操作,你要經過用戶的同意。調用wx.getSetting來判斷用戶是否已經授權,如果沒有授權,就要讓他點擊按鈕授權,同意之后你就能進行下面的操作了。返回值中只會出現小程序已經向用戶請求過的權限。
參數
object.success回調函數
參數
示例代碼
lu:function(){ wx.getSetting({ #查詢用戶已經授權的所有操作 success(res) { if (!res.authSetting['scope.record']) { #回調函數authSetting查詢用戶是否授權了"scope.record" 這個scope wx.authorize({ scope: 'scope.record', success() { // 用戶已經同意小程序使用錄音功能,后續調用 wx.startRecord 接口不會彈窗詢問 wx.startRecord() #調用小程序錄音功能 } }) }else{ wx.startRecord() } } }) }
回調函數AuthSetting官方文檔:https://developers.weixin.qq.com/miniprogram/dev/api/open-api/setting/AuthSetting.html
注意事項:
1.wx.authorize({scope:"scope.userInfo"}),不會彈出授權窗口,請在頁面使用<button_open-type="getUserInfo"> 2.需要授權 scope.userLocation、scope.userLocationBackground 時必須配置地理位置用途說明(https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/app.html#permission)
針對於上面的代碼中,還有wx.authorize
提前向用戶發起授權請求。調用后會立刻彈窗詢問用戶是否同意授權小程序使用某項功能或獲取用戶的某些數據,但不會實際調用對應接口。如果用戶之前已經同意授權,則不會出現彈窗,直接返回成功。更多用法詳見 用戶授權。
參數
3.wx.getUserinfo() 獲取用戶信息,解密獲取一些用戶敏感信息
調用前需要用戶授權scope.userInfo
參數
object.lang的合法值
object.success回調函數
參數
接口調整說明
在用戶未授權的情況下調用此接口,將不再出現授權彈窗,會直接進入fail回調。在用戶已經授權店額情況下調用此接口,可成功獲取用戶信息。
示例代碼
小程序代碼
info:function(res){ wx.checkSession({ success() { //必須是已經授權情況下使用 wx.getUserInfo({ success:function(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() //重新登錄 } }) }
后台
小程序把需要的數據都傳遞過來了,現在需要在后台校驗與解密開放數據。
微信會對這些開放的數據做簽名和加密處理。開發者后台拿到開放數據后可以對數據進行校驗簽名和解密,來保證數據不被篡改。
簽名校驗以及數據加解密涉及用戶的會話密鑰 session_key。 開發者應該事先通過 wx.login 登錄流程獲取會話密鑰 session_key 並保存在服務器。為了數據不被篡改,開發者不應該把 session_key 傳到小程序客戶端等服務器外的環境。
數據簽名校驗
為了確保開放接口返回用戶數據的安全性,微信會對明文數據進行簽名。開發者可以根據業務需要對數據包進行簽名校驗,確保數據的完整性。
- 通過調用接口(如 wx.getUserInfo)獲取數據時,接口會同時返回 rawData、signature,其中 signature = sha1( rawData + session_key )
- 開發者將 signature、rawData 發送到開發者服務器進行校驗。服務器利用用戶對應的 session_key 使用相同的算法計算出簽名 signature2 ,比對 signature 與 signature2 即可校驗數據的完整性。
如 wx.getUserInfo的數據校驗:
接口返回的rawData:
{ "nickName": "Band", "gender": 1, "language": "zh_CN", "city": "Guangzhou", "province": "Guangdong", "country": "CN", "avatarUrl": "http://wx.qlogo.cn/mmopen/vi_32/1vZvI39NWFQ9XM4LtQpFrQJ1xlgZxx3w7bQxKARol6503Iuswjjn6nIGBiaycAjAtpujxyzYsrztuuICqIM5ibXQ/0" }
用戶的 session-key:
HyVFkGl5F5OQWJZZaNzBBg==
用於簽名的字符串為:
{"nickName":"Band","gender":1,"language":"zh_CN","city":"Guangzhou","province":"Guangdong","country":"CN","avatarUrl":"http://wx.qlogo.cn/mmopen/vi_32/1vZvI39NWFQ9XM4LtQpFrQJ1xlgZxx3w7bQxKARol6503Iuswjjn6nIGBiaycAjAtpujxyzYsrztuuICqIM5ibXQ/0"}HyVFkGl5F5OQWJZZaNzBBg==
使用sha1得到的結果為
75e81ceda165f4ffa64f4068af58c64b8f54b88c
加密數據解密算法
接口如果涉及敏感數據(如wx.getUserInfo當中的 openId 和 unionId),接口的明文內容將不包含這些敏感數據。開發者如需要獲取敏感數據,需要對接口返回的加密數據(encryptedData) 進行對稱解密。 解密算法如下:
- 對稱解密使用的算法為 AES-128-CBC,數據采用PKCS#7填充。
- 對稱解密的目標密文為 Base64_Decode(encryptedData)。
- 對稱解密秘鑰 aeskey = Base64_Decode(session_key), aeskey 是16字節。
- 對稱解密算法初始向量 為Base64_Decode(iv),其中iv由數據接口返回。
微信官方提供了多種編程語言的示例代碼((點擊下載)。每種語言類型的接口名字均一致。調用方式可以參照示例。
另外,為了應用能校驗數據的有效性,會在敏感數據加上數據水印( watermark )
watermark參數說明:
如接口 wx.getUserInfo 敏感數據當中的 watermark:
{ "openId": "OPENID", "nickName": "NICKNAME", "gender": GENDER, "city": "CITY", "province": "PROVINCE", "country": "COUNTRY", "avatarUrl": "AVATARURL", "unionId": "UNIONID", "watermark": { "appid":"APPID", "timestamp":TIMESTAMP } }
注:
- 解密后得到的json數據根據需求可能會增加新的字段,舊字段不會改變和刪減,開發者需要預留足夠的空間
會話密鑰 session_key 有效性
開發者如果遇到因為 session_key 不正確而校驗簽名失敗或解密失敗,請關注下面幾個與 session_key 有關的注意事項。
- wx.login 調用時,用戶的 session_key 可能會被更新而致使舊 session_key 失效(刷新機制存在最短周期,如果同一個用戶短時間內多次調用 wx.login,並非每次調用都導致 session_key 刷新)。開發者應該在明確需要重新登錄時才調用 wx.login,及時通過 auth.code2Session 接口更新服務器存儲的 session_key。
- 微信不會把 session_key 的有效期告知開發者。我們會根據用戶使用小程序的行為對 session_key 進行續期。用戶越頻繁使用小程序,session_key 有效期越長。
- 開發者在 session_key 失效時,可以通過重新執行登錄流程獲取有效的 session_key。使用接口 wx.checkSession可以校驗 session_key 是否有效,從而避免小程序反復執行登錄流程。
- 當開發者在實現自定義登錄態時,可以考慮以 session_key 有效期作為自身登錄態有效期,也可以實現自定義的時效性策略。
后台代碼示例
加密數據解密代碼 (在微信小程序文檔中下載的)
WXBizDataCrypt.py
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)
serializers.py
from rest_framework.serializers import ModelSerializer from app01 import models class User_ser(ModelSerializer): class Meta: model=models.Wxuser fields="__all__"
views.py
class GetInfo(APIView): def post(self,request): param=request.data if param['encryptedData'] and param['iv'] and param['login_key']: #小程序傳遞的參數 openid,seesion_key=cache.get(param['login_key']).split("&") 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'], } 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 #serializer校驗 return Response({"code":200,"msg":"缺少參數","data":data}) #用戶信息傳到小程序 else: return Response({"code":200,"msg":"缺少參數"})