微信小程序的登入与授权
官方文档: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":"缺少参数"})