小程序登录流程全解析

6,863 阅读4分钟

用户登录是一个系统的必备功能。而小程序的登录流程和Web端又有一些不同,主要是要与微信服务器进行通信验证。下面我们就来看下小程序具体的登录流程。

1. 登录流程

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

小程序登录时序图

2. 登录逻辑:

  1. 调用 wx.login() 获取 临时登录凭证 code,有效期为 5分钟;(临时登录凭证 code 只能使用一次)

  2. 将临时 code 传到我们的后端,后端调用 auth.code2Session 接口,换取用户唯一标识 OpenID 和 会话密钥 session_key;( openid 是用户唯一标识,session_key 能保证当前用户进行会话操作的有效性)

    注意:获取 session_key 出于安全性的考虑,要在后端调用。如果我们在前端通过 request 调用此接口,就不可避免的需要将我们小程序的appidsecret 和服务端下发的 session_key 暴露在外部,会给我们的业务安全带来极大的风险。 session_key 拥有一定的时效性。用户越久未使用小程序,用户登录态越有可能失效。反之如果用户一直在使用小程序,则用户登录态一直保持有效。具体时效逻辑由微信维护,对开发者透明。开发者需要调用 wx.checkSession 接口检测当前用户登录态是否有效。

  3. 后端自定义新的密钥并关联返回的 session_keyopenid,将新的密钥返给前端,前端将其存储在 storage 中。(会话密钥 session_key 是对用户数据进行 加密签名 的密钥。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥,所以要定义新的密钥)。 之所以存在storage中,是因为小程序没有 cookie,相应的后端 set-cookie 在小程序中不起作用。

  4. 前端发送请求的时候,带着密钥,后端根据密钥识别用户身份,返回数据。

3. 遇到的问题

在接口报401错误时,对其做登录处理,登录成功后再次调用接口。这种情况下,多接口并行就会出现问题。 因为多接口都会调 wx.login 获取 code 后再调后端的登录接口,而登录凭证 code 只能使用一次,这时候后端对多接口就会接收到不同的 code,返回的自定义密钥就会有多个,不能保持统一。

处理的方案是后端在一定时间内对用户自定义登录态做缓存。接口调 wx.login 获取 code 后再调后端的登录接口时,判断后端缓存中有无此用户,如果没有,返回新的密钥,如果缓存中存在此用户并在缓存有效时间内,就查找并返回之前的密钥。

4. 具体代码

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

  checkSession: function () {
    return wx.pro.checkSession().then(res => {
      return Promise.resolve('result1')
    }).catch(err => {
      return this.login()
    })
  },
  
  login: function () {
    return wx.pro.login().then(res => {
      return Promise.resolve(res.code)
    }).then(res => {
      return this.getRequest({
        method: 'POST',
        url: "***/login",
        data: {
          code: res
        }
      })
    }).then(res => {
      let Cookie = res && res.header && res.header['Set-Cookie'] || '';
      Cookie = this.getCookie('cookie-key', Cookie)
      wx.setStorageSync('Cookie', Cookie)
      this.globalData.Cookie = Cookie
      return Promise.resolve('result2')
    }).catch(err => {
      console.log('loginErr', err)
    })
  },
  
  getCookie(name, cookie) {
    let arr, reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)");
    return (arr = cookie.match(reg)) ? unescape(arr[2]) : null;
  },
  
  getRequest: function (data, tryNum = 1) {
    let header = Object.assign({
      "Content-Type": "application/x-www-form-urlencoded",
    }, data.header)
    if (data.url !== '**/login') {
      header = Object.assign(header, {
        "Cookie": `cookie-key=${this.globalData.Cookie}`
      })
    }
    return new Promise((resolve, reject) => {
      wx.request({
        url: (data.mode !== 'debug' ? Env_config[envObj.env].api : '') + data.url,
        method: data.method || "GET",
        header: header,
        data: data.data,
        success: (res) => {
          if (res && res.statusCode === 200) {
            resolve(res)
          } else if (res.statusCode === 401) {
            wx.showToast({
              title: '登录失效,请重新登录',
              icon: 'none',
            })
            if (tryNum > maxTryNum) return null
            return this.login().then(res => {
              return this.getRequest(data, ++tryNum)
            }).then(res => resolve(res))
                .catch(err => {
                  reject(err)
                })
          } else {
            setTimeout(() => {
              wx.showToast({
                title: res && res.data && res.data.msg || '服务异常,请稍后重试',
                icon: 'none'
              })
            }, 0)
            reject(res)
          }
        },
        fail: (err) => {
          setTimeout(() => {
            wx.showToast({
              title: res && res.data && res.data.msg || '服务异常,请稍后重试',
              icon: 'none'
            })
          }, 0)
          reject(err)
        }
      })
    })
  },

在页面中的用法也很简单:

onLoad: function (options) {
  app.checkSession().then(res => {
    this.otherMethods()
  })
}