微信小程序开发系列-05登录小程序

发布时间:2023年12月27日

本文继续学习下微信小程序登录相关的内容。

微信平台小程序用户体系

普通用户视角:对于每个小程序,微信都会将用户的微信ID映射出一个小程序OpenID,作为这个用户在这个小程序的唯一标识。(注意:同一微信用户在不同小程序的openid不同。)

开发者视角:对于拥有多个小程序的开发者,开发者可以注册一个微信开放平台账号,然后把所有小程序绑定在这一个开放平台下,这样的话微信还会将微信ID映射出一个UnionID,作为这个用户在整个开放平台的唯一ID。(注:该能力不单限于小程序,所有公众号、网站、移动APP,只要使用微信开放能力登录的应用,都能获取到unionid,这样就能打通各个平台的用户账号体系了。)

登录流程时序

请添加图片描述

说明

  1. 调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
  2. 调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台账号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台账号) 和 会话密钥 session_key

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

注意事项

  1. 会话密钥 session_key 是对用户数据进行 加密签名 的密钥。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥
  2. 临时登录凭证 code 只能使用一次

以上是官方给出的登录时序说明,要理解上述内容,还需要学习其中涉及的很多细节,并实际编写代码运行体会。

相关接口介绍

wx.login

获取用户登录凭证(code),有效期五分钟。

开发者需要在开发者服务器后台调用 code2Session,使用 code 换取 openid、unionid、session_key 等信息。

OpenID:用户在当前小程序的唯一标识。

UnionID:微信开放平台账号下的唯一标识。(若当前小程序已绑定到微信开放平台账号,没绑定的话没有)

sesion_key:本次登录的会话密钥。

wx.login({
  success (res) {
    if (res.code) {
      //发起网络请求
      wx.request({
        url: 'https://example.com/onLogin',
        data: {
          code: res.code
        }
      })
    } else {
      console.log('登录失败!' + res.errMsg)
    }
  }
})

wx.getSetting

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

可获取用户当前的授权状态。

属性类型说明最低版本
authSettingAuthSetting用户授权结果
subscriptionsSettingSubscriptionsSetting用户订阅消息设置,接口参数withSubscriptions值为true时才会返回。2.10.1
miniprogramAuthSettingAuthSetting在插件中调用时,当前宿主小程序的用户授权结果

请添加图片描述

请添加图片描述

AuthSetting

用户授权设置信息

属性作用接口
boolean scope.userInfo是否授权用户信息wx.getUserInfo
boolean scope.userLocation是否授权精确地理位置wx.getLocation, wx.chooseLocation
boolean scope.userFuzzyLocation是否授权模糊地理位置wx.getFuzzyLocation
boolean scope.address是否授权通讯地址,已取消此项授权,会默认返回true
boolean scope.invoiceTitle是否授权发票抬头,已取消此项授权,会默认返回true
boolean scope.invoice是否授权获取发票,已取消此项授权,会默认返回true
boolean scope.werun是否授权微信运动步数wx.getWeRunData
boolean scope.record是否授权录音功能wx.startRecord
boolean scope.writePhotosAlbum是否授权保存到相册wx.saveImageToPhotosAlbum, wx.saveVideoToPhotosAlbum
boolean scope.camera是否授权摄像头camera ((camera)) 组件
boolean scope.bluetooth是否授权蓝牙wx.openBluetoothAdapterwx.createBLEPeripheralServer
boolean scope.addPhoneContact是否添加通讯录联系人wx.addPhoneContact
boolean scope.addPhoneCalendar是否授权系统日历wx.addPhoneRepeatCalendarwx.addPhoneCalendar

wx.authorize

提前向用户发起授权请求。调用后会立刻弹窗询问用户是否同意授权小程序使用某项功能或获取用户的某些数据,但不会实际调用对应接口。如果用户之前已经同意授权,则不会出现弹窗,直接返回成功。

getSetting(e){
    wx.getSetting({
      success: (res) => {
        console.log(res)
        if (!res.authSetting['scope.record']){
          wx.authorize({
            scope: 'scope.record',
            success: ()=>{
              const options = {
                duration: 10000,
                sampleRate: 44100,
                numberOfChannels: 1,
                encodeBitRate: 192000,
                format: 'aac',
                frameSize: 50
              }
              wx.getRecorderManager().start(options)
            }
          })
        }
      }
    })
  },

授权后再调用wx.getSetting就可以看到scope.record为true了。

请添加图片描述

开放数据校验与解密

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

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

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

请添加图片描述

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

数据签名校验

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

  1. 通过调用接口(如 wx.getUserInfo)获取数据时,接口会同时返回 rawData、signature,其中 signature = sha1( rawData + session_key )
  2. 开发者将 signature、rawData 发送到开发者服务器进行校验。服务器利用用户对应的 session_key 使用相同的算法计算出签名 signature2 ,比对 signature 与 signature2 即可校验数据的完整性。
加密数据解密算法

接口如果涉及敏感数据(如 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参数说明:

参数类型说明
appidString敏感数据归属 appId,开发者可校验此参数与自身 appId 是否一致
timestampInt敏感数据获取的时间戳, 开发者可以用于数据时效性校验
会话密钥 session_key 有效性

开发者如果遇到因为 session_key 不正确而校验签名失败或解密失败,请关注下面几个与 session_key 有关的注意事项。

  1. wx.login 调用时,用户的 session_key 可能会被更新而致使旧 session_key 失效(刷新机制存在最短周期,如果同一个用户短时间内多次调用 wx.login,并非每次调用都导致 session_key 刷新)。开发者应该在明确需要重新登录时才调用 wx.login,及时通过 code2Session 接口更新服务器存储的 session_key。
  2. 微信不会把 session_key 的有效期告知开发者。我们会根据用户使用小程序的行为对 session_key 进行续期。用户越频繁使用小程序,session_key 有效期越长。
  3. 开发者在 session_key 失效时,可以通过重新执行登录流程获取有效的 session_key。使用接口 wx.checkSession可以校验 session_key 是否有效,从而避免小程序反复执行登录流程。
  4. 当开发者在实现自定义登录态时,可以考虑以 session_key 有效期作为自身登录态有效期,也可以实现自定义的时效性策略。

也就是说,在小程序服务端,通过验签和解密可以获取到敏感数据(如用户信息等)。

wx.checkSession

检查登录状态是否过期。

通过 wx.login 接口获得的用户登录态拥有一定的时效性。用户越久未使用小程序,用户登录态越有可能失效。反之如果用户一直在使用小程序,则用户登录态一直保持有效。具体时效逻辑由微信维护,对开发者透明。开发者只需要调用 wx.checkSession 接口检测当前用户登录态是否有效。

总结

微信小程序登录流程

微信小程序的登录流程大体上涉及三个部分:小程序客户端、小程序服务端(开发者自行搭建)、微信服务端。

请添加图片描述

小程序客户端通过wx.login获取临时登录凭证code,然后通过wx.request将code发送给小程序服务端,小程序服务端再通过code2session接口获取OpenId、session_key等。到这里为止,其实微信小程序本身的登录已经完成了,后续的流程只是小程序应用根据实际需求来对用户进行管理,这个部分就看小程序应用的开发者依据项目需求实际进行开发了。

微信小程序登录步骤

  1. 小程序客户端通过wx.login 获取临时登录凭证code
  2. 小程序客户端将code传给小程序服务端;
  3. 小程序服务端通过 auth.code2Session接口,获取三个东西:
    1. OpenID:用户唯一标识;
    2. UnionID: 微信开放平台账号下的唯一标识;(绑定微信开放平台后才有)
    3. session_key:会话密钥;

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

注意事项

  1. 会话密钥 session_key 是对用户数据进行 加密签名 的密钥。为了应用自身的数据安全,小程序服务端不应该把会话密钥下发到小程序应用端,也不应该对外提供这个密钥
  2. 临时登录凭证 code 有效期5分钟。

核心代码

小程序客户端代码

index.wxml
<!--pages/index/index.wxml-->
<view class="container">
  <view class="userinfo">
    <block wx:if="{{!hasUserInfo}}">
      <button wx:if="{{canIUseGetUserProfile}}" bindtap="getUserProfile"> 获取头像昵称 </button>
    </block>
    <block wx:else>
      <image bindtap="bindViewTap" class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image>
      <text class="userinfo-nickname">{{userInfo.nickName}}</text>
    </block>
    <input type="nickname" placeholder="请输入昵称"/>
  </view>
  <button bindtap="openSetting"> 打开设置页 </button>
  <button bindtap="getSetting">获取设置</button>
</view>
Index.js
// pages/index/index.js
Page({

  /**
   * 页面的初始数据
   */
  data: {
    userInfo: {},
    hasUserInfo: false,
    canIUseGetUserProfile: false,
    code:''
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
    if (wx.getUserProfile) {
      this.setData({
        canIUseGetUserProfile: true
      })
    }
     wx.login({
      success: (resp) => {
        console.log("code:", resp)
        this.setData({
          code:resp.code
        })
      }
    })
  },
  getSetting(e){
    wx.getSetting({
      success: (res) => {
        console.log(res)
        /*
        if (!res.authSetting['scope.record']){
          wx.authorize({
            scope: 'scope.record',
            success: ()=>{
              const options = {
                duration: 10000,
                sampleRate: 44100,
                numberOfChannels: 1,
                encodeBitRate: 192000,
                format: 'aac',
                frameSize: 50
              }
              wx.getRecorderManager().start(options)
            }
          })
        }
        */
      }
    })
  },
  openSetting(e){
    wx.openSetting({
      success: (res) => {
        console.log(res)
      }
    })
  },
  getUserProfile(e) {
    wx.getUserProfile({
      desc: '用于完善会员资料', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
      success: (res) => {
        console.log("success:", res)
        this.setData({
          userInfo: res.userInfo,
          hasUserInfo: true
        })
        wx.request({
          url: 'http://127.0.0.1:5000/login',
          data: {
            code: this.data.code,
            encryptedData: res.encryptedData,
            iv: res.iv
          },
          method: "POST",
          success(res) {
            console.log(res);
          },
          fail(res) {
            console.log(res)
          }
        })
      },
      fail: (res) => {
        console.log("fail:", res)
      }
    })
  },
  /**
   * 生命周期函数--监听页面初次渲染完成
   */
  onReady() {

  },

  /**
   * 生命周期函数--监听页面显示
   */
  onShow() {

  },

  /**
   * 生命周期函数--监听页面隐藏
   */
  onHide() {

  },

  /**
   * 生命周期函数--监听页面卸载
   */
  onUnload() {

  },

  /**
   * 页面相关事件处理函数--监听用户下拉动作
   */
  onPullDownRefresh() {

  },

  /**
   * 页面上拉触底事件的处理函数
   */
  onReachBottom() {

  },

  /**
   * 用户点击右上角分享
   */
  onShareAppMessage() {

  }
})

请添加图片描述

点击“获取头像昵称”按钮后,可以从小程序服务端获取到userinfo,带watermark。

请添加图片描述

小程序服务端代码

test.py
import requests
from flask import Flask, jsonify, request
from WXBizDataCrypt import WXBizDataCrypt

app = Flask(__name__)

@app.route("/login", methods = ['POST'])
def login():
    data = request.get_json()
    print(data)
    appID = '你自己的appID' #开发者关于微信小程序的appID
    appSecret = '你自己的appSecret' #开发者关于微信小程序的appSecret
    code = data['code'] #前端POST过来的微信临时登录凭证code    
    encryptedData = data['encryptedData']
    iv = data['iv']
    req_params = {
        'appid': appID,
        'secret': appSecret,
        'js_code': code,
        'grant_type': 'authorization_code'
    }
    wx_login_api = 'https://api.weixin.qq.com/sns/jscode2session'
    response_data = requests.get(wx_login_api, params=req_params) #向API发起GET请求
    data2 = response_data.json()
    print(data2)
    if (data2['openid']):
        openid = data2['openid'] #得到用户关于当前小程序的OpenID
        session_key = data2['session_key'] #得到用户关于当前小程序的会话密钥session_key
        pc = WXBizDataCrypt(appID, session_key) #对用户信息进行解密
        userinfo = pc.decrypt(encryptedData, iv) #获得用户信息
        print(userinfo)
    else:
        data = "get openid failed"

    return jsonify(result = data)


if __name__ == '__main__':
    app.run()

WXBizDataCrypt.py
import base64
import json
from Crypto.Cipher import AES

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:])]

文章来源:https://blog.csdn.net/weixin_43708622/article/details/135254607
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。