Editor’s note: The author of this article is Feng Tong, senior front-end development engineer of Strange Dance Group

User login is a required process for most full apps

A simple user system needs to focus on at least these aspects

  • Security (encryption)
  • Persistent login state (similar to cookies)
  • Login Expiration Processing
  • Ensure user uniqueness and avoid multiple accounts
  • authorization
  • Bind user information, such as the user name profile picture
  • Binding mobile phone number (real name and secret security)

Many business requirements can be abstracted into Restful interfaces with CRUD operations

However, the login process is complicated, and each platform has its own process, which can become a time-consuming part of the project, such as the login process of small programs

For a project starting from scratch, getting the log-in process right is a good start, and a good start is half the battle

In this paper, micro channel small program this platform, about a complete custom user login process, to chew this difficult to chew bone

Noun explanation

Start with a brief explanation of the nouns in the sequence diagram of the login process

  • codeTemporary login credentials, valid for five minutes. Passwx.login()To obtain
  • session_keySession key that the server passescode2SessionTo obtain
  • openIdThe unique identity of the user in this small program, which is always the same, is obtained by the server through code
  • unionIdThe unique identity of a user under the same wechat open platform account (public account, mini program, website, mobile application) will never change
  • appIdUnique identification of applets
  • appSecretApp secret of the small program can be exchanged with code and appId for session_key

Other nouns

  • rawDataRaw data string, excluding sensitive information, used to compute the signature
  • encryptedDataUser information, which contains sensitive information, is encrypted
  • signatureUsed to check whether user information is not tampered
  • ivThe initial vector of the encryption algorithm

What is sensitive information? Mobile phone number, openId, unionId, it can be seen that these values can uniquely locate a user, while nicknames, avatar and other information that cannot locate a user are not sensitive information

Applets login related functions

  • wx.login
  • wx.getUserInfo
  • wx.checkSession

The promise of the applet

We found that the asynchronous interfaces of applets were all callback to Success and fail, making it easy to write callback hell

So you can start by simply implementing a WX asynchronous function into a promise utility function

const promisify = original= > {
  return function(opt) {
    return new Promise((resolve, reject) = > {
      opt = Object.assign({
        success: resolve,
        fail: reject
      }, opt)
      original(opt)
    })
  }
}
Copy the code

So we can call the function like this

promisify(wx.getStorage)({key: 'key'}).then(value= > {
  // success
}).catch(reason= > {
  // fail
})
Copy the code

Server-side implementation

The demo server implementation is based on express.js

Note that for the sake of demo simplicity, the server uses JS variables to store user data, meaning that if the server is restarted, the user data is wiped out

To store user data persistently, you can implement database-related logic by yourself

// Store all user information
const users = {
  // openId as index
  openId: {
    // The data structure is as follows
    openId: ' '.// In theory it should not be returned to the front end
    sessionKey: ' '.nickName: ' '.avatarUrl: ' '.unionId: ' '.phoneNumber: ' '
  }
}

app
  .use(bodyParser.json())
  .use(session({
    secret: 'alittlegirl'.resave: false.saveUninitialized: true
  }))
Copy the code

Applets login

Let’s start by implementing a basic Oauth authorized login

Oauth authorized login is the process of code exchanging openId and sessionKey

Front-end applets login

Write in the app. In js

login () {
  console.log('login')
  return util.promisify(wx.login)().then(({code}) = > {
    console.log(`code: ${code}`)
    return http.post('/oauth/login', {
      code,
      type: 'wxapp'})})}Copy the code

The server implements Oauth authorization

The server implements the above /oauth/login interface

app
  .post('/oauth/login', (req, res) => {
    var params = req.body
    var {code, type} = params
    if (type === 'wxapp') {
      // code for openId and sessionKey
      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 = users[openId]
        if(! user) { user = { openId,sessionKey: data.session_key
          }
          users[openId] = user
          console.log('New user', user)
        } else {
          console.log('Regular users', user)
        }
        req.session.openId = user.openId
        req.user = user
      }).then((a)= > {
        res.send({
          code: 0})})}else {
      throw new Error('Unknown authorization type')}})Copy the code

Obtaining User information

There is an important function in the login system: get user information, we call it getUserInfo

If the user is logged in, getUserInfo returns user information, such as a nickname, avatar, etc., if not logged in, “User not logged in.”

In other words, this interface can also determine whether the user is logged in or not…

Applet user information is typically stored in app.globalData.userInfo (template)

We add front-end middleware on the server side to obtain the corresponding user information through session and put it in the REQ object

app
  .use((req, res, next) = > {
    req.user = users[req.session.openId]
    next()
  })
Copy the code

The /user/info interface is then implemented to return user information

app
  .get('/user/info', (req, res) => {
    if (req.user) {
      return res.send({
        code: 0.data: req.user
      })
    }
    throw new Error('User not logged in')})Copy the code

The applet calls the user information interface

getUserInfo () {
  return http.get('/user/info').then(response= > {
    let data = response.data
    if (data && typeof data === 'object') {
      // If the user information is obtained successfully, it is saved globally
      this.globalData.userInfo = data
      return data
    }
    return Promise.reject(response)
  })
}
Copy the code

A library designed to make requests for small programs

Applets make requests through HTTP. get, http.post and other apis, using a request library behind them

@chunpu/ HTTP is an HTTP request library designed specifically for applets that can make requests on applets just like AXIOS, with support for interceptors and other powerful features, and even more handy than AXIOS

The initialization method is as follows

import http from '@chunpu/http'

http.init({
  baseURL: 'http://localhost:9999'.// Define baseURL for local testing
  wx // The tag is for wechat applet
})
Copy the code

Please refer to github.com/chunpu/http…

Custom log-in persistence

Browsers have cookies, but small programs do not have cookies, so how to imitate the login state like a web page?

This uses the applet’s own persistence interfaces, namely setStorage and getStorage

In order to facilitate each end to share the interface, or directly reuse the Web interface, we have implemented a simple read cookie and cookie logic

The cookie is planted based on the HTTP Response headers returned. Here we use the response interceptor from @chunpu/ HTTP, the same as axios

http.interceptors.response.use(response= > {
  / / kind of cookies
  var {headers} = response
  var cookies = headers['set-cookie'] | |' '
  cookies = cookies.split(/, */).reduce((prev, item) = > {
    item = item.split(/; * /) [0]
    var obj = http.qs.parse(item)
    return Object.assign(prev, obj)
  }, {})
  if (cookies) {
    return util.promisify(wx.getStorage)({
      key: 'cookie'
    }).catch((a)= > {}).then(res= > {
      res = res || {}
      var allCookies = res.data || {}
      Object.assign(allCookies, cookies)
      return util.promisify(wx.setStorage)({
        key: 'cookie'.data: allCookies
      })
    }).then((a)= > {
      return response
    })
  }
  return response
})
Copy the code

Of course we also need to send all cookies with the request, using the Request interceptor

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

Validity period of the login state

We know that login cookies in browsers have an expiration date, such as one day, seven days, or a month

Perhaps some friends will ask, directly use storage, small program login state validity period how to do?

That’s the point! The small program has helped us to realize the validity of the session wx.checkSession

It’s smarter than cookies, as described in the official documentation

The login status obtained through the Wx. login interface has a certain timeliness. The longer the user does not use the applets, the more likely it is that the user login state will fail. Otherwise, if the user has been using the mini program, the user login status remains valid

That is to say, the small program will help us automatically renew our login state, it is artificial intelligence cookie, click a like 👍

So how do you do that on the front end? The code is written in app.js

onLaunch: function () {
  util.promisify(wx.checkSession)().then((a)= > {
    console.log('the session to take effect)
    return this.getUserInfo()
  }).then(userInfo= > {
    console.log('Login successful', userInfo)
  }).catch(err= > {
    console.log('Automatic login failed, log in again', err)
    return this.login()
  }).catch(err= > {
    console.log('Manual login failed', err)
  })
}
Copy the code

Note that the session is not only the login state of the front end, but also the validity period of the session_key of the back end. If the login state of the front end fails, the back end also fails and the session_key needs to be updated

In theory, it is possible for applets to customize login expiration time policies, but in this case we need to consider the developer’s own expiration time and the applets interface service expiration time, so it is easier to keep the same

Ensure that userInfo is available for each Page

If you select Create a normal Quick launch template in the New Applet project

We will get a template that we can run directly

If you look at the code, most of it is dealing with userInfo….

It says in the notes

Since getUserInfo is a network request, it may not return until after Page.onload

Callback is added here to prevent this

But this template is not scientific, so only consider the home page needs user information, if the scan code into the page also needs user information? There is a direct jump into the unpaid page active page…

If every page is checked to see if the user information has been loaded, the code is too redundant

$(function) is the ready function of jQuery. If the document is ready, you can execute the code inside the function. If the document is not ready, you can execute the code after it is ready

That’s the idea! We treat the App of the applet as the document of the web page

Our goal is to be able to get userInfo from the Page in this way without error

Page({
  data: {
    userInfo: null
  },
  onLoad: function () {
    app.ready((a)= > {
      this.setData({
        userInfo: app.globalData.userInfo
      })
    })
  }
})
Copy the code

Here we use min-ready to do this

The code implementation is still written in app.js

import Ready from 'min-ready'

const ready = Ready()

App({
  getUserInfo () {
    // Get user information as a global method
    return http.get('/user/info').then(response= > {
      let data = response.data
      if (data && typeof data === 'object') {
        this.globalData.userInfo = data
        // The successful retrieval of userInfo is app ready
        ready.open()
        return data
      }
      return Promise.reject(response)
    })
  },
  ready (func) {
    // Put the function into the queue
    ready.queue(func)
  }
})
Copy the code

Bind user information and mobile phone numbers

It’s not enough just to get a user’s openId. OpenId can only tag the user, not even their nickname or avatar

How do you retrieve this user information and store it in a back-end database?

We implement these two interfaces on the server side, bind user information, bind user mobile phone number

app
  .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('User not logged in')
  })

  .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('User not logged in')})Copy the code

The applets personal center WXML implementation is as follows

<view wx:if="userInfo" class="userinfo">
  <button
    wx:if="{{! userInfo.nickName}}"
    type="primary"
    open-type="getUserInfo"
    bindgetuserinfo="bindUserInfo">Get avatar nickname</button>
  <block wx:else>
    <image class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image>
    <text class="userinfo-nickname">{{userInfo.nickName}}</text>
  </block>

  <button
    wx:if="{{! userInfo.phoneNumber}}"
    type="primary"
    style="margin-top: 20px;"
    open-type="getPhoneNumber"
    bindgetphonenumber="bindPhoneNumber">Bind mobile phone number</button>
  <text wx:else>{{userInfo.phoneNumber}}</text>
</view>
Copy the code

The bindUserInfo and bindPhoneNumber functions in the mini program, according to the latest policy of wechat, both of these operations need the user to click the button for unified authorization to trigger

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

code

The code mentioned in this article can be found on my Github

Small program code in wxapp-login-demo

Server-side Node.js code in wxapp-login-server

About Weird Dance Weekly

Qiwu Weekly is a front-end technology community operated by qiwu Group, a professional front-end team of 360 Company. After paying attention to the public number, directly send the link to the background can contribute to us.