全面解析微信小程序开发之登录∕授权

体验过小程序的童鞋应该都知道,最初进入一个新的小程序,会让我们进行一系列授权操作,同意过后就可以正常使用了,并且在下次进入的时候依然保持授权后的状态

那这一系列操作是怎么完成的呢?我们的授权登入态是怎么维护的呢?

下面就来说说我根据官方给出的方法来写出的维护登录态的方法吧

登录态维护

微信小程序的运行环境不是在浏览器下运行的,所以就不能用浏览器下那套cookie实现机制来维护登录态了,但我们可以仿浏览器来实现

官方文档给出了wx.login()的方法来获取用户登录态:

登录时序图

image

  1. 小程序在调用wx.login()时会获取到临时登录凭证code,然后将code发送到后台服务器

    前端具体实现如下(app.js)

    // 登录
    login() {
      return util.promisify(wx.login)().then(({ code }) => {
        return http.post('/oauth/login', {
        code,
        type: 'wxapp'
        })
      }).then(({ data }) => {
        return this.getUserInfo()
      })
    }
    
  2. 后台服务器再通过code去请求微信服务器,换取对应的用于用户唯一标识的openid和会话密钥(解密用户的敏感信息)session_key

    后端具体实现如下(server.js)

    axios.get('https://api.weixin.qq.com/sns/jscode2session', {
      params: {
        appid: config.appId,
        secret: config.appSecret,
        js_code: code,
        grant_type: 'authorization_code'
      }
    }).then(({data}) => {
      var openId = data.openid
      var user = userList.find(user => {
        return user.openId === openId
      })
    
  3. 后台服务器紧接着可以根据用户标识来生成自定义登录态,以便于维护后续业务逻辑中前后端交互时的身份

    需要注意的是,后台服务器在拿到session_key、openid等字段时,不应该直接作为用户的标识或者session的标识,而应该自己生成一个session登录态(可以参考登录时序图),在保证其安全性的角度上不设置较长的过期时间

    后端具体实现如下(server.js)

    var sessionMap = {}
    
    app
      .use(bodyParser.urlencoded({ extended: false }))
      .use(bodyParser.json())
      .use(session({
        secret: 'alittlegirl',
        resave: false,
        saveUninitialized: true
      }))
    
      .use((req, res, next) => {
        req.user = sessionMap[req.session.id] // 通过不同的session.id映射不同的user
        console.log(`req.url: ${req.url}`)
        if (req.user) {
          console.log(`wxapp openId`, req.user.openId)
        } else {
          console.log(`session`, req.session.id)
        }
        next()
      })
    
  4. session通过响应头派发到小程序客户端之后,可将其存储在storage中,然后每次发请求时再加在请求头上,用于后续通信的登录态校验

    前端具体实现如下(util.js)

    响应头拦截器

    http.interceptors.response.use(response => {
      var { headers, data, status } = response
      if (data && typeof data === 'object') {
        Object.assign(response, data)
        if (data.code !== 0) {
          return Promise.reject(new Error(data.message || 'error'))
        }
      }
      if (status >= 400) {
        return Promise.reject(new Error('error'))
      }
      var setCookie = headers['set-cookie'] || ''
      var cookie = setCookie.split('; ')[0]
      if (cookie) {
        var cookie = qs.parse(cookie)
        return util.promisify(wx.getStorage)({
          key: 'cookie'
        }).catch(() => { }).then(res => {
          res = res || {}
          var allCookie = res.allCookie || {}
          Object.assign(allCookie, cookie)
          return util.promisify(wx.setStorage)({
            key: 'cookie',
            data: allCookie
          })
        }).then(() => {
          return response
        })
      } else {
        return response
      }
    })
    

    请求头拦截器

    http.interceptors.request.use(config => {
      return util.promisify(wx.getStorage)({
        key: 'cookie'
      }).catch(() => { }).then(res => {
        if (res && res.data) {
          Object.assign(config.headers, {
            Cookie: qs.stringify(res.data, ';', '=')
          })
        }
        return config
      })
      return config
    })
    

以上就是整个登录态维护的实现方法,总结来说就是

  • 调用wx.login()得到code后请求服务器获取openidsession_key缓存到服务器当中

  • 其中生成一个随机数为keyvalueopenidsession_key

  • 然后返回到小程序通过wx.setStorage('token',得到的随机数key)存在小程序当中

  • 每当我们去请求服务器时带上token即可给服务器读取从而判断用户是否在登录

自定义登录态有效期

上述维护登录态的实现,我们已经了解到最关键的是生成与openidsession_key对应的key值,来保持用户唯一身份的识别

而我们每调用一次wx.login()时,就会更新用户的session_key,从而导致旧的session_key失效

所以我们必须明确只有在需要重新登录时才调用wx.login()

那怎么确定什么时候才是需要重新登录的呢?换句话说就是怎么知道session_key的有效期呢?

我们可以先看下官方文档

微信不会把session_key的有效期告知开发者。我们会根据用户使用小程序的行为对session_key进行续期。用户越频繁使用小程序,session_key有效期越长。

开发者在session_key失效时,可以通过重新执行登录流程获取有效的session_key。使用接口wx.checkSession()可以校验session_key是否有效,从而避免小程序反复执行登录流程。

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

也就是说小程序通过wx.login()接口获得的用户登录态是具有一定的时效性的,可以通过wx.checkSession()来判断登录态是否过期,从而决定是否更新登录态

前端具体实现如下(app.js)

onLaunch: function () {
  console.log('build time', date.formatTime(new Date()))
  util.promisify(wx.checkSession)().then(() => {
    console.log('session 有效')
    return this.getUserInfo()
  }).then((userInfo) => {
    console.log('登录成功', userInfo)
  }).catch(() => {
    console.log('自动登录失败')
    return this.login()
  }).catch(err => {
    console.log(`手动登录失败`)
  })
},

微信授权&获取用户数据

通过wx.login()实现登录态维护之后,我们就可以通过wx.getUserInfo()获取用户信息、wx.getPhoneNumber()获取手机号信息了

得到的用户数据包含两部分,一部分是敏感信息,需要从密文中解密出来,密文在encryptedData这个字段中,去请求后台服务器去解密然后就可以得到敏感信息并保存下来了;另一部分是不敏感的信息,在result的userInfo里

如下图所示

image

有一点需要注意的是,微信5月份出了最新的策略,这俩操作都需要用户主动点击点击按钮,完成统一授权后才能触发

wxml代码如下(index. wxml)

<view class="container">
  <view class="userinfo">
    <view wx:if="{{!userInfo.nickName}}">
      <button 
        open-type="getUserInfo" 
        bindgetuserinfo="bindUserInfo"> 
        获取头像昵称 
      </button>
    </view>
    <block wx:else>
      <image bindtap="bindViewTap" class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover" bindtap='bindViewTap'></image>
      <text class="userinfo-nickname">{{userInfo.nickName}}</text>
    </block>
  </view>

  <view class="userinfo">
    <view wx:if="{{!userInfo.phoneNumber}}">
      <button
        open-type="getPhoneNumber" 
        bindgetphonenumber="bindPhoneNumber">
          获取手机号码
      </button>     
    </view>
    <text wx:else>{{userInfo.phoneNumber}}</text>
  </view> 
  <view class="usermotto">
    <text class="user-motto">{{motto}}</text>
  </view>
</view>

在实际项目开发中,我们每进行一次不同的授权操作,都会将微信服务器请求授权的用户数据返回到后台服务器,然后写入统一的用户信息接口,再返回给小程序

每次授权后前端也都需要进行用户信息更新,以便于将所有有关用户的信息都存于统一的接口,便于调用

如果每进行一次授权操作,都重新写一遍请求后台用户信息接口的逻辑,难免让代码显得冗余

那正确的操作是怎样的呢?

我们可以在入口app.js文件中声明调用的后台接口,将用户信息存入全局定义的数组中(暂且称之为userInfo)

前端代码如下(app.js)

// 获取用户信息
getUserInfo() {
  return http.get('/user/info').then(({ data }) => {
    if (data && typeof data === 'object') {
      this.globalData.userInfo = data
      // 延时函数
      if (this.userInfoReadyCallback) {
        this.userInfoReadyCallback(data)
      }
      return data
    }
    return Promise.reject(response)
  })
},

globalData: {
  userInfo: null
}

这样即使在授权过后,再次使用该小程序时,我们也能够从全局直接拿到用户信息,然后在各个页面调用全局数据即可

比如微信授权页面(index.js)

onLoad: function () {
  if (app.globalData.userInfo) {
    this.setData({
      userInfo: app.globalData.userInfo
    })
  }else if (this.data.canIUse){
    // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回
    // 所以此处加入 callback 以防止这种情况
    app.userInfoReadyCallback = res => {
      this.setData({
        userInfo: res
      })
    }
  } else {
    // 在没有 open-type=getUserInfo 版本的兼容处理
    wx.getUserInfo({
      success: res => {
        app.globalData.userInfo = res.userInfo
        this.setData({
          userInfo: res.userInfo
        })
      }
    })
  }
}

那前后端是怎么实现微信授权操作中从微信服务器读取数据然后存入后台数据库的呢?

获取用户信息(头像昵称等)

前端代码如下(index.js)

bindUserInfo: function(e) {
  var detail = e.detail
  if (detail.iv) {
    http.post('/user/bindinfo', {
      encryptedData: detail.encryptedData
      iv: detail.iv
      signature: detail.signature
    }).then(()=>{
      return app.getUserInfo().then(userInfo => {
        this.setData({
          userInfo: userInfo
        })
      })
    })
  }
}

服务端接口代码如下(server.js)

.post('/user/bindinfo', (req, res) => {
  var user = req.user
  if (user) {
    var {encryptedData, iv} = req.body
    var pc = new WXBizDataCrypt(config.appId, user.sessionKey)
    var data = pc.decryptData(encryptedData, iv)
    Object.assign(user, data)
    return res.send({
      code: 0
    })
  }
  throw new Error('用户未登录')
})

获取用户手机号

前端代码如下(inde.js)

bindPhoneNumber(e) {
  var detail = e.detail
  if (detail.iv) {
    http.post('/user/bindphone', {
      encryptedData: detail.encryptedData,
      iv: detail.iv
    }).then(() => {
      return app.getUserInfo().then(userInfo => {
        this.setData({
          userInfo: userInfo
        })
      })
    })
  }
}

服务端接口如下(server.js)

.post('/user/bindphone', (req, res) => {
  var user = req.user
  if (user) {
    var {encryptedData, iv} = req.body
    var pc = new WXBizDataCrypt(config.appId, user.sessionKey)
    var data = pc.decryptData(encryptedData, iv)
    Object.assign(user, data)
    return res.send({
      code: 0
    })
  }
  throw new Error('用户未登录')
})

代码

可见我的wxapp-authorization-demo