一.小程序登录
登入 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__" # 所有字段,如果是部分序列化就用列表