Title

微信小程序的登陆和授权,以及用户信息解密

小程序登录

小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识,快速建立小程序内的用户体系。

登录流程时序

 

 

说明:

  1. 调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。

  2. 调用 auth.code2Session 接口,换取 用户唯一标识 OpenID会话密钥 session_key

之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。

注意:

  1. 会话密钥 session_key 是对用户数据进行 加密签名 的密钥。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥

  2. 临时登录凭证 code 只能使用一次

openid与unionid

openid:是用单个微信应用表示用户的唯一标识。亚洲:饼哥小程序上openid :123,那该用户再张成的小程序上他的opendid不是123,是其他任意一个值,上面的意思:同一用户再不用不同应用上的openid不同,但是再同一应用上唯一。


场景: 假设你们公司有2个小程序。但是你们老板想把用户做统一处理。比如新用户登入任意一个小程序,就发送发送礼包。但是只要再一个小程序上领过了,就不能再另一个上面领取。
unionnid:一个用户在多个小程序有唯一的标识

官方文档:小程序的登入(wx.login(Object object))

调用接口获取登录凭证(code)。通过凭证进而换取用户登录态信息,包括用户的唯一标识(openid)及本次登录的会话密钥(session_key)等。用户数据的加解密通讯需要依赖会话密钥完成。更多使用方法详见 小程序登录

参数

Object object

属性类型默认值必填说明最低版本
timeout number   超时时间,单位ms 1.9.90
success function   接口调用成功的回调函数  
fail function   接口调用失败的回调函数  
complete function   接口调用结束的回调函数(调用成功、失败都会执行)  

object.success 回调函数

参数
Object res
属性类型说明
code string 用户登录凭证(有效期五分钟)。开发者需要在开发者服务器后台调用 auth.code2Session,使用 code 换取 openid 和 session_key 等信息

 

 

 

第一步:小程序获取code并通过request向服务端发送code

代码演示

小程序编译器:app.js

App({
 onLaunch: function () {
 let that = this
   // 登录
   wx.login({
     success: res => {
       // 发送 res.code 到后台换取 openId, sessionKey, unionId
       console.log(res.code)
       //通过wx.request来发送code
         wx.request({
         url: that.globalData.baseurl+"login/",
         data:{"code":res.code},
         method:"POST",
         success(e){
           console.log(e)
        }
      })
    }
  })    
   console.log("小程序初始化")
  },
   onShow: function (option) {
     console.log("小程序onshow,:onShow", option)
  },
 globalData: {
   userInfo: null,
   //设置全局基础路由  
   baseurl:"http://127.0.0.1:8000/"
}
})

pycharm:

# 全局路由配置urls.py
from django.conf.urls import url
from django.contrib import admin
from app01.view import test,user
urlpatterns = [
   url(r'^admin/', admin.site.urls),
   url(r'^test/', test.Test.as_view()),
   url(r'^login/', user.Login.as_view()),
]

# appp01/view/user.py
from rest_framework.views import APIView
from rest_framework.response import Response

class Login(APIView):
   def post(self, request):
       param = request.data
       print(param)
       return Response({"status": 0, "data": param})
   
# 打印结果
{'code': '023WEz2v08k2Cg1ylG4v0O3R2v0WEz28'}
[12/Mar/2020 18:32:46] "POST /login/ HTTP/1.1" 200 63

此时已完成小程序获取code并通过request向服务端发送code

第二步:接下来开始调用auth.code2Session接口来获取 用户唯一标识 OpenID会话密钥 session_key

 

官方文档:auth.code2Session接口

本接口应在服务器端调用,详细说明参见服务端API

登录凭证校验。通过 wx.login 接口获得临时登录凭证 code 后传到开发者服务器调用此接口完成登录流程。更多使用方法详见 小程序登录

 

请求地址

GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code

请求参数

属性类型默认值必填说明
appid string   小程序 appId
secret string   小程序 appSecret
js_code string   登录时获取的 code
grant_type string   授权类型,此处只需填写 authorization_code

返回值

Object

返回的 JSON 数据包

属性类型说明
openid string 用户唯一标识
session_key string 会话密钥
unionid string 用户在开放平台的唯一标识符,在满足 UnionID 下发条件的情况下会返回,详见 UnionID 机制说明
errcode number 错误码
errmsg string 错误信息

errcode 的合法值

说明最低版本
-1 系统繁忙,此时请开发者稍候再试  
0 请求成功  
40029 code 无效  
45011 频率限制,每个用户每分钟100次  

代码演示:

pycharm中

app01/wx/wx_settings.py

AppID = "wx0e6b084777c39552"
AppSecret = "aa875e6293963ef5d0166753afcea248"
# https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
# 将上面官网给的请求地址填到接口中,并将部分参数格式化,在wx.login中导入
code2Session = "https://api.weixin.qq.com/sns/jscode2session?appid={}&secret={}&js_code={}&grant_type=authorization_code"

app01/wx/wx_login.py

from app01.wx import settings
import requests


def get_login_info(code):
   code_url = settings.code2Session.format(settings.AppID, settings.AppSecret, code)
   # 通过request发送请求并获取响应(响应为json数据包)
   response = requests.get(code_url)
   json_response = response.json()
   print("json_response",json_response)
   # 打印结果:json_response {'session_key': '3u/Y+ysYY5rX5Ws8Uf8J2A==', 'openid': 'o-Xsa43CuFhNt3ADKPCBw4VjQULY'}
   if json_response.get("session_key"):
       return json_response
   else:
       return False

app01/view/user.py

from rest_framework.views import APIView
from rest_framework.response import Response
from app01.wx import wx_login

class Login(APIView):
   def post(self, request):
       param = request.data
       if not param.get("code"):
           return Response({"status": 1, "msg": "缺少参数code"})
       else:
           code = param.get("code")
           # user_data就是返回的openid和session_key
           user_data = wx_login.get_login_info(code)
           print(user_data)
           #打印结果:{'session_key': '3u/Y+ysYY5rX5Ws8Uf8J2A==', 'openid': 'o-Xsa43CuFhNt3ADKPCBw4VjQULY'}
           return Response({"status": 0, "data": param})

第三步:获取到openid和session_key后,自定义登陆状态,绑定session_key和openid

settings.py

# 配置redis
CACHES = {
   'default':{
       'BACKEND':'django_redis.cache.RedisCache',
       'LOCATION':'redis://127.0.0.1:6379',
       "OPTIONS":{
           "CLIENT_CLASS":"diango_redis.client.DefaultClient",
           "PASSWORD":"",
      },
  },
}

app01/view/user.py

from rest_framework.views import APIView
from rest_framework.response import Response
from app01.wx import wx_login

import hashlib, time
from django.core.cache import cache
from app01.models import Wxuser
class Login(APIView):
   def post(self, request):
       param = request.data
       if not param.get("code"):
           return Response({"status": 1, "msg": "缺少参数code"})
       else:
           code = param.get("code")
           # 获取session_key 和 openid
           user_data = wx_login.get_login_info(code)
           
           if user_data:
               val = user_data['session_key'] + "&" + user_data['openid']
               md5 = hashlib.md5()
               md5.update(str(time.clock()).encode('utf-8'))
               # 加盐获取唯一标识key
               md5.update(user_data["session_key"].encode('utf-8'))
               key = md5.hexdigest()
               # 将key和value放在redis中
               cache.set(key, val)
               # 因为openid后面订单需求中可以用来查表,所以放在数据库中一份,需要查询数据库里是否有唯一openid,没有则存进去
               hash_user = Wxuser.objects.filter(openid=user_data['openid']).first()
               if not hash_user:
                   Wxuser.objects.create(openid=user_data['openid'])
               return Response({
                   "status":0,
                   "msg":"ok",
                   # 将key返回回去用做唯一标识
                   "data":{"token":key}
              })
           else:
               return Response({"status": 2, "msg": "无效的code"})

小程序app.js

App({
 onLaunch: function () {
   // // 展示本地存储能力
   // var logs = wx.getStorageSync('logs') || []
   // logs.unshift(Date.now())
   // wx.setStorageSync('logs', logs)
 let that = this
   // 登录
   wx.login({
     success: res => {
       // 发送 res.code 到后台换取 openId, sessionKey, unionId
       console.log(res.code)
       wx.request({
         url: that.globalData.baseurl+"login/",
         data:{"code":res.code},
         method:"POST",
         success(e){
              //将token存储在本地
           wx.setStorageSync('token', e.data.data.token)
        }
      })
    }
  })
   // // 获取用户信息
   // wx.getSetting({
   //   success: res => {
   //     if (res.authSetting['scope.userInfo']) {
   //       // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框
   //       wx.getUserInfo({
   //         success: res => {
   //           // 可以将 res 发送给后台解码出 unionId
   //           this.globalData.userInfo = res.userInfo

   //           // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回
   //           // 所以此处加入 callback 以防止这种情况
   //           if (this.userInfoReadyCallback) {
   //             this.userInfoReadyCallback(res)
   //           }
   //         }
   //       })
   //     }
   //   }
   // })
   console.log("小程序初始化")
  },
   onShow: function (option) {
     console.log("小程序onshow,:onShow", option)
  },
 globalData: {
   userInfo: null,
   baseurl:"http://127.0.0.1:8000/"
}
})

总结:

1 小程序端执行wx.login()获取code
2 将1中的code发送到后端,后端调用auth.code2Session这个接口,得到openid和session_key
3 自定义登入状态,传给前端一个唯一的标识key,下次前端访问的时候带着标识,如果能在数据库中找到即符合要求,我们生成一个key与openid和session_key相绑定。把key返回到小程序中
4 小程序端保存,然后下次请求需要登入的接口的时候,把key带上。(通过key就能获取session_key和openid,再通过openid就能获取用户信息)

小程序授权

官方文档:

授权

部分接口需要经过用户授权同意才能调用。我们把这些接口按使用范围分成多个 scope ,用户选择对 scope 来进行授权,当授权给一个 scope 之后,其对应的所有接口都可以直接使用。

此类接口调用时:

  • 如果用户未接受或拒绝过此权限,会弹窗询问用户,用户点击同意后方可调用接口;

  • 如果用户已授权,可以直接调用接口;

  • 如果用户已拒绝授权,则不会出现弹窗,而是直接进入接口 fail 回调。请开发者兼容用户拒绝授权的场景。

获取用户授权设置

开发者可以使用 wx.getSetting 获取用户当前的授权状态。

下面关于wx.getSetting接口信息官方文档

wx.getSetting(Object object)

基础库 1.2.0 开始支持,低版本需做兼容处理

获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限

参数

Object object

属性类型默认值必填说明最低版本
withSubscriptions Boolean false 是否同时获取用户订阅消息的订阅状态,默认不获取。注意:withSubscriptions 只返回用户勾选过订阅面板中的“总是保持以上选择,不再询问”的订阅消息。 2.10.1
success function   接口调用成功的回调函数  
fail function   接口调用失败的回调函数  
complete function   接口调用结束的回调函数(调用成功、失败都会执行)  

object.success 回调函数

参数
Object res
属性类型说明最低版本
authSetting AuthSetting 用户授权结果  
subscriptionsSetting SubscriptionsSetting 用户订阅消息设置,接口参数withSubscriptions值为true时才会返回。 2.10.1

提前发起授权请求

开发者可以使用 wx.authorize 在调用需授权 API 之前,提前向用户发起授权请求。

打开设置界面

用户可以在小程序设置界面(「右上角」 - 「关于」 - 「右上角」 - 「设置」)中控制对该小程序的授权状态。

开发者可以调用 wx.openSetting 打开设置界面,引导用户开启授权。

授权有效期

一旦用户明确同意或拒绝过授权,其授权关系会记录在后台,直到用户主动删除小程序。

最佳实践

在真正需要使用授权接口时,才向用户发起授权申请,并在授权申请中说明清楚要使用该功能的理由。

注意事项

  1. wx.authorize({scope: "scope.userInfo"}),不会弹出授权窗口,请使用 ``

  2. 需要授权 scope.userLocationscope.userLocationBackground 时必须配置地理位置用途说明

后台定位

与其它类型授权不同的是,scope.userLocationBackground 不会弹窗提醒用户。需要用户在设置页中,主动将“位置信息”选项设置为“使用小程序期间和离开小程序后”。开发者可以通过调用wx.openSetting,打开设置页。

scope 列表

scope对应接口描述
scope.userInfo wx.getUserInfo 用户信息
scope.userLocation wx.getLocation, wx.chooseLocation 地理位置
scope.userLocationBackground wx.startLocationUpdateBackground 后台定位
scope.address wx.chooseAddress 通讯地址
scope.invoiceTitle wx.chooseInvoiceTitle 发票抬头
scope.invoice wx.chooseInvoice 获取发票
scope.werun wx.getWeRunData 微信运动步数
scope.record wx.startRecord 录音功能
scope.writePhotosAlbum wx.saveImageToPhotosAlbum, wx.saveVideoToPhotosAlbum 保存到相册
scope.camera camera 组件 摄像头

代码演示

page.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()
      }
    }
  })
},

 

 

 

 

 

总结:

1 因为部分功能需要用同意后才能使用。

2 wx.getSetting来判断该用户有没有对接口授权,我判断哪个接口,就必须给wx.getSetting传对应的scope值
- 一个scope值对应这个一个或多个接口

3 如果我们从wx.getSetting中发现scope值是false,标识没有授权,我们可以通过wx.authorize发起授权,对那个接口授权,就给wx.authorize传对应scope值就可以了。如果用用户同意授权,就可以直接使用对应的接口了。

4 但是scope.userInfo没有办法使用wx.authorize自动弹起弹框。必须要用户手动点击按钮唤起授权弹框。
代码格式:
<button open-type="getUserInfo" bindgetuserinfo="user1">用户信息</button>
我们可以再响应函数的参数中获取用户信息。e.detail,这个和直接调用wx.getUserInfo获取的内容一样。

'''
user1: function (e) {
  console.log("e",e.detail)
  wx.getSetting({
    success(res) {
      if (res.authSetting['scope.userInfo']) {
        wx.getUserInfo({
          success(res) {
            // 用户已经同意小程序使用录音功能,后续调用 wx.startRecord 接口不会弹窗询问
            console.log("res",res)
          }
        })
      }
    }
  })
},
'''

====================================================================================================================================================

官方文档:

wx.getUserInfo(Object object)

调用前需要 用户授权 scope.userInfo。

获取用户信息。

参数

Object object

属性类型默认值必填说明
withCredentials boolean   是否带上登录态信息。当 withCredentials 为 true 时,要求此前有调用过 wx.login 且登录态尚未过期,此时返回的数据会包含 encryptedData, iv 等敏感信息;当 withCredentials 为 false 时,不要求有登录态,返回的数据不包含 encryptedData, iv 等敏感信息。
lang string en 显示用户信息的语言
success function   接口调用成功的回调函数
fail function   接口调用失败的回调函数
complete function   接口调用结束的回调函数(调用成功、失败都会执行)

object.lang 的合法值

说明最低版本
en 英文  
zh_CN 简体中文  
zh_TW 繁体中文  

object.success 回调函数

参数
Object res
属性类型说明最低版本
userInfo UserInfo 用户信息对象,不包含 openid 等敏感信息  
rawData string 不包括敏感信息的原始数据字符串,用于计算签名  
signature string 使用 sha1( rawData + sessionkey ) 得到字符串,用于校验用户信息,详见 用户数据的签名验证和加解密  
encryptedData string 包括敏感数据在内的完整用户信息的加密数据,详见 用户数据的签名验证和加解密  
iv string 加密算法的初始向量,详见 用户数据的签名验证和加解密  
cloudID string 敏感数据对应的云 ID,开通云开发的小程序才会返回,可通过云调用直接获取开放数据,详细见云调用直接获取开放数据 2.7.0

接口调整说明

在用户未授权过的情况下调用此接口,将不再出现授权弹窗,会直接进入 fail 回调(详见《公告》)。在用户已授权的情况下调用此接口,可成功获取用户信息。

示例代码

// 必须是在用户已经授权的情况下调用
wx.getUserInfo({
 success: function(res) {
   var userInfo = res.userInfo
   var nickName = userInfo.nickName
   var avatarUrl = userInfo.avatarUrl
   var gender = userInfo.gender //性别 0:未知、1:男、2:女
   var province = userInfo.province
   var city = userInfo.city
   var country = userInfo.country
}
})

敏感数据有两种获取方式,一是使用 加密数据解密算法 。 获取得到的开放数据为以下 json 结构:

{
 "openId": "OPENID",
 "nickName": "NICKNAME",
 "gender": GENDER,
 "city": "CITY",
 "province": "PROVINCE",
 "country": "COUNTRY",
 "avatarUrl": "AVATARURL",
 "unionId": "UNIONID",
 "watermark": {
   "appid":"APPID",
   "timestamp":TIMESTAMP
}
}

小程序用户信息组件示例代码

<!-- 如果只是展示用户头像昵称,可以使用 <open-data /> 组件 -->
<open-data type="userAvatarUrl"></open-data>
<open-data type="userNickName"></open-data>
<!-- 需要使用 button 来授权登录 -->
<button wx:if="{{canIUse}}" open-type="getUserInfo" bindgetuserinfo="bindGetUserInfo">授权登录</button>
<view wx:else>请升级微信版本</view>
Page({
data: {
  canIUse: wx.canIUse('button.open-type.getUserInfo')
},
onLoad: function() {
  // 查看是否授权
  wx.getSetting({
    success (res){
      if (res.authSetting['scope.userInfo']) {
        // 已经授权,可以直接调用 getUserInfo 获取头像昵称
        wx.getUserInfo({
          success: function(res) {
            console.log(res.userInfo)
          }
        })
      }
    }
  })
},
bindGetUserInfo (e) {
  console.log(e.detail.userInfo)
}
})

==========================================================================

==========================================================================

官方文档

服务端获取开放数据

小程序可以通过各种前端接口获取微信提供的开放数据。考虑到开发者服务端也需要获取这些开放数据,微信提供了两种获取方式:

 

方式一:开发者后台校验与解密开放数据

微信会对这些开放数据做签名和加密处理。开发者后台拿到开放数据后可以对数据进行校验签名和解密,来保证数据不被篡改。

img

签名校验以及数据加解密涉及用户的会话密钥 session_key。 开发者应该事先通过 wx.login 登录流程获取会话密钥 session_key 并保存在服务器。为了数据不被篡改,开发者不应该把 session_key 传到小程序客户端等服务器外的环境。

数据签名校验

为了确保开放接口返回用户数据的安全性,微信会对明文数据进行签名。开发者可以根据业务需要对数据包进行签名校验,确保数据的完整性。

  1. 通过调用接口(如 wx.getUserInfo)获取数据时,接口会同时返回 rawData、signature,其中 signature = sha1( rawData + session_key )

  2. 开发者将 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) 进行对称解密。 解密算法如下:

  1. 对称解密使用的算法为 AES-128-CBC,数据采用PKCS#7填充。

  2. 对称解密的目标密文为 Base64_Decode(encryptedData)。

  3. 对称解密秘钥 aeskey = Base64_Decode(session_key), aeskey 是16字节。

  4. 对称解密算法初始向量 为Base64_Decode(iv),其中iv由数据接口返回。

注意:下载示例代码

微信官方提供了多种编程语言的示例代码((点击下载)。每种语言类型的接口名字均一致。调用方式可以参照示例。**

另外,为了应用能校验数据的有效性,会在敏感数据加上数据水印( watermark )

watermark参数说明:

参数类型说明
appid String 敏感数据归属 appId,开发者可校验此参数与自身 appId 是否一致
timestamp Int 敏感数据获取的时间戳, 开发者可以用于数据时效性校验

如接口 wx.getUserInfo 敏感数据当中的 watermark:

{
   "openId": "OPENID",
   "nickName": "NICKNAME",
   "gender": GENDER,
   "city": "CITY",
   "province": "PROVINCE",
   "country": "COUNTRY",
   "avatarUrl": "AVATARURL",
   "unionId": "UNIONID",
   "watermark":
  {
       "appid":"APPID",
       "timestamp":TIMESTAMP
  }
}

注:

  1. 解密后得到的json数据根据需求可能会增加新的字段,旧字段不会改变和删减,开发者需要预留足够的空间

会话密钥 session_key 有效性

开发者如果遇到因为 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. 开发者在 session_key 失效时,可以通过重新执行登录流程获取有效的 session_key。使用接口 wx.checkSession可以校验 session_key 是否有效,从而避免小程序反复执行登录流程。

  4. 当开发者在实现自定义登录态时,可以考虑以 session_key 有效期作为自身登录态有效期,也可以实现自定义的时效性策略。

 

方式二:云调用直接获取开放数据

接口如果涉及敏感数据(如wx.getWeRunData),接口的明文内容将不包含这些敏感数据,而是在返回的接口中包含对应敏感数据的 cloudID 字段,数据可以通过云函数获取。完整流程如下:

1. 获取 cloudID

使用 2.7.0 或以上版本的基础库,如果小程序已开通云开发,在开放数据接口的返回值中可以通过 cloudID 字段获取(与 encryptedData 同级),cloudID 有效期五分钟。

2. 调用云函数

调用云函数时,对传入的 data 参数,如果有顶层字段的值为通过 wx.cloud.CloudID 构造的 CloudID,则调用云函数时,这些字段的值会被替换为 cloudID 对应的开放数据,一次调用最多可替换 5 个 CloudID

示例:

在小程序获取到 cloudID 之后发起调用:

wx.cloud.callFunction({
 name: 'myFunction',
 data: {
   weRunData: wx.cloud.CloudID('xxx'), // 这个 CloudID 值到云函数端会被替换
   obj: {
     shareInfo: wx.cloud.CloudID('yyy'), // 非顶层字段的 CloudID 不会被替换,会原样字符串展示
  }
}
})

在云函数收到的 event 示例:

// event
{
// weRunData 的值已被替换为开放数据
"weRunData": {
  "cloudID": "xxx",
  "data": {
    "stepInfoList": [
      {
        "step": 5000,
        "timestamp": 1554814312,
      }
    ],
    "watermark": {
      "appid": "wx1111111111",
      "timestamp": 1554815786
    }
  }
},
"obj": {
  // 非顶层字段维持原样
  "shareInfo": "yyy",
}
}

如果 cloudID 非法或过期,则在 event 中获取得到的将是一个有包含错误码、错误信息和原始 cloudID 的对象。过期 cloudID 换取结果示例:

// event
{
"weRunData": {
  "cloudID": "xxx",
  "errCode": -601006,
  "errMsg": "cloudID expired."
},
// ...
}

====================================================================================================================================================

微信关于session_key密钥的有效性,可以通过wx.checkSession校验

代码演示:微信小程序部分

# app.js

//app.js
App({
 onLaunch: function () {
   // this指的是当前App对象,调用下面的my_login方法
   this.my_login()  
  },
   onShow: function (option) {
     console.log("小程序onshow,:onShow", option)
  },

   my_login:function(){
     let that = this
     // 登录
     wx.login({
       success: res => {
         // 发送 res.code 到后台换取 openId, sessionKey, unionId
         console.log(res.code)
         wx.request({
           url: that.globalData.baseurl + "login/",
           data: { "code": res.code },
           method: "POST",
           success(e) {
             //将token存储在本地
             wx.setStorageSync('token', e.data.data.token)
          }
        })
      }
    })
  },
   // 表示全局可用
 globalData: {
   userInfo: null,
   baseurl:"http://127.0.0.1:8000/"
}
})



# page.js

// pages/test3/test3.js
//全局任何地方都可以通过getApp()获取App.js中的app对象
const app = getApp()
Page({

 /**
  * 页面的初始数据
  */
 data: {

},
 user1: function (e) {
   console.log("e",e.detail)
   wx.getSetting({
     success(res) {
       if (res.authSetting['scope.userInfo']) {
         wx.checkSession({
           success() {
             // session_key 未过期,并且在本生命周期一直有效
             wx.getUserInfo({
               success(res) {
                 console.log("res", res)
                   //朝后台getinfo路由发送数据
                 wx.request({
                   url: 'app. globalData.baseurl+"getinfo/"',
                   data:{
                     iv:res.iv,
                     encryptedData: res.encryptedData,
                     token: wx.getStorageSync("token"),              
                  },
                   method:"POST",
                   success:(e)=>{
                     console.log("后台返回的数据",e)
                  }
                })
              }
            })
          },
           fail() {
             // session_key 已经失效,需要重新执行登录流程
           app.my_login() //重新登录
             wx.getUserInfo({
               success(res) {
                 console.log("res", res)
                 wx.request({
                   url: 'app. globalData.baseurl+"getinfo/"',
                   data:{
                     iv:res.iv,
                     encryptedData: res.encryptedData,
                     token: wx.getStorageSync("token"),              
                  },
                   method:"POST",
                   success:(e)=>{
                     console.log("后台返回的数据",e)
                  }
                })
              }
            })
          }
        })        
      }
    }
  })
},
})

后端使用官方提供的sdk,进行解密。

# 如官方的sdk没有Crypto包用下面的方法解决
pip install pycryptodome

解密代码演示:django后台部分

#路由
# urls.py
from django.conf.urls import url
from django.contrib import admin
from app01.view import test,user
urlpatterns = [
   url(r'^admin/', admin.site.urls),
   url(r'^test/', test.Test.as_view()),
   url(r'^login/', user.Login.as_view()),
   url(r'^getinfo/', user.Info.as_view()),
]



# 解密模块
#app01/wx/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:])]

   
   # 就是上面decrypt方法的简化(可根据官方解密文档中的python示例)
   @classmethod
   def get_info(cls, sessionKey, encryptedData, iv):
       # appId = settings.AppID
       # sessionKey = sessionKey
       # encryptedData = encryptedData
       # iv = iv

       # pc = cls(appId, sessionKey)
       # pc = cls(settings.AppID, sessionKey)

       # print pc.decrypt(encryptedData, iv)
       return cls(settings.AppID, sessionKey).decrypt(encryptedData, iv)



   
   

# app01/view/user.py

# 获取信息及解码
from rest_framework.views import APIView
from rest_framework.response import Response
from app01.wx import wx_login

import hashlib, time
from django.core.cache import cache
from app01.models import Wxuser

# 导入解密模块
from app01.wx import WXBizDataCrypt
# 导入序列化
from app01.my_ser import wx_user_ser

# 登陆验证
class Login(APIView):
   def POST(self, request):
       param = request.data
       if not param.get("code"):
           return Response({"status": 1, "msg": "缺少参数code"})
       else:
           code = param.get("code")
           # 获取session_key 和 openid
           user_data = wx_login.get_login_info(code)
           if user_data:
               val = user_data['session_key'] + "&" + user_data['openid']
               md5 = hashlib.md5()
               md5.update(str(time.clock()).encode('utf-8'))
               # 加盐获取唯一标识key
               md5.update(user_data["session_key"].encode('utf-8'))
               key = md5.hexdigest()
               # 将key和value放在redis中
               cache.set(key, val)
               # 因为openid后面订单需求中可以用来查表,所以放在数据库中一份,需要查询数据库里是否有唯一openid,没有则存进去
               hash_user = Wxuser.objects.filter(openid=user_data['openid']).first()
               if not hash_user:
                   Wxuser.objects.create(openid=user_data['openid'])
               return Response({"status": 0, "msg": "ok",  # 将key返回回去用做唯一标识
                                "data": {"token": key}})
           else:
               return Response({"status": 2, "msg": "无效的code"})

# 获取用户信息(解密)
class Info(APIView):
   def post(self, request):
       param = request.data
       # 先验证参数是否传过来了
       if param.get('iv') and param.get('token') and param.get('encryptedData'):
           # 验证token
           session_key_openid = cache.get(param.get("token"))
           if session_key_openid:
               # 切分获得session_key,openid
               session_key, openid = session_key_openid.split("&")
               # 调用解密包解密
               user_info = WXBizDataCrypt.WXBizDataCrypt.get_info(session_key, param.get('encryptedData'),
                                                                  param.get('iv'))
               # 解密后就能拿到下面的信息
               save_data = {
                   "name": user_info['nickName'],
                   "avatar": user_info['avatarUrl'],
                   "language": user_info['language'],
                   "province": user_info['province'],
                   "city": user_info['city'],
                   "country": user_info['country'], }
               # 将其存入数据库中(注意:有些信息是比如是头像或图片,数据库的数据格式应当修改为utf8mb4格式)
               Wxuser.objects.filter(openid=openid).update(**save_data)
               # 如何检验头像是否存入,将其再取出返回给前台,若正常返回则没有问题
               user = Wxuser.objects.filter(openid=openid).first()
               user = wx_user_ser(instance=user, many=False).data
               return Response({"status": 0, "msg": "ok", "data": user})
           else:
               return Response({"code": 2, "msg": "无效的token"})
       else:
           return Response({"code": 1, "msg": "缺少参数"})

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2020-03-12 23:56  Mr江  阅读(1951)  评论(0编辑  收藏  举报