彻底搞懂微信小程序登录流程-附小程序和服务端代码

原文链接: http://www.vdongchina.com/read/id/2827.html

微信小程序登录流程实现

用户登录是大部分完整 App 必备的流程,一个简单的用户系统需要关注至少这些层面

安全性(加密)

持久化登录态(类似cookie)

登录过期处理

确保用户唯一性, 避免出现多账号

授权

绑定用户昵称头像等信息

绑定手机号(实名和密保方式)

很多的业务需求都可以抽象成 Restful 接口配合 CRUD 操作

但登录流程却是错综复杂, 各个平台有各自的流程, 反倒成了项目中费时间的部分, 比如小程序的登录流程

彻底搞懂微信小程序登录流程-附小程序和服务端代码_第1张图片

对于一个从零开始的项目来说, 搞定登录流程, 就是一个好的开始, 一个好的开始, 就是成功的一半

本文就以微信小程序这个平台, 讲述一个完整的自定义用户登录流程, 一起来啃这块难啃的骨头

名词解释

先给登录流程时序图中出现的名词简单做一个解释

code 临时登录凭证, 有效期五分钟, 通过 wx.login() 获取

session_key 会话密钥, 服务端通过 code2Session 获取

openId 用户在该小程序下的用户唯一标识, 永远不变, 服务端通过 code 获取

unionId 用户在同一个微信开放平台帐号(公众号, 小程序, 网站, 移动应用)下的唯一标识, 永远不变

appId 小程序唯一标识

appSecret 小程序的 app secret, 可以和 code, appId 一起换取 session_key

其他名词

rawData 不包括敏感信息的原始数据字符串,用于计算签名

encryptedData 包含敏感信息的用户信息, 是加密的

signature 用于校验用户信息是否无篡改

iv 加密算法的初始向量

哪些信息是敏感信息呢? 手机号, openId, unionId, 可以看出这些值都可以唯一定位一个用户, 而昵称,头像这些不能定位用户的都不是敏感信息

小程序登录相关函数

wx.login

wx.getUserInfo

wx.checkSession

小程序的 promise

我们发现小程序的异步接口都是 success 和 fail 的回调, 很容易写出回调地狱

因此可以先简单实现一个 wx 异步函数转成 promise 的工具函数

const promisify = original => {

return function(opt) {

return new Promise((resolve, reject) => {

opt = Object.assign({

success: resolve,

fail: reject

}, opt)

original(opt)

})

}

}

这样我们就可以这样调用函数了

promisify(wx.getStorage)({key: 'key'}).then(value => {

// success

}).catch(reason => {

// fail

})

服务端实现

本 demo 的服务端实现基于 express.js

注意, 为了 demo 的简洁性, 服务端使用 js 变量来保存用户数据, 也就是说如果重启服务端, 用户数据就清空了

如需持久化存储用户数据, 可以自行实现数据库相关逻辑

// 存储所有用户信息

const users = {

// openId 作为索引

openId: {

// 数据结构如下

openId: '', // 理论上不应该返回给前端

sessionKey: '',

nickName: '',

avatarUrl: '',

unionId: '',

phoneNumber: ''

}

}

app

.use(bodyParser.json())

.use(session({

secret: 'alittlegirl',

resave: false,

saveUninitialized: true

}))

小程序登录

我们先实现一个基本的 oauth 授权登录

oauth 授权登录主要是 code 换取 openId 和 sessionKey 的过程

前端小程序登录

写在 app.js 中

login () {

console.log('登录')

return util.promisify(wx.login)().then(({code}) => {

console.log(`code: ${code}`)

return http.post('/oauth/login', {

code,

type: 'wxapp'

})

})

}

服务端实现 oauth 授权

服务端实现上述 /oauth/login 这个接口

app

.post('/oauth/login', (req, res) => {

var params = req.body

var {code, type} = params

if (type === 'wxapp') {

// code 换取 openId 和 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('新用户', user)

} else {

console.log('老用户', user)

}

req.session.openId = user.openId

req.user = user

}).then(() => {

res.send({

code: 0

})

})

} else {

throw new Error('未知的授权类型')

}

})

获取用户信息

登录系统中都会有一个重要的功能: 获取用户信息, 我们称之为 getUserInfo

如果已登录用户调用 getUserInfo 则返回用户信息, 比如昵称, 头像等, 如果未登录则返回'用户未登录'

也就是说此接口还有判断用户是否登录的功效...

小程序的用户信息一般存储在 app.globalData.userInfo 中(模板如此)

我们在服务端加上前置中间件, 通过 session 来获取对应的用户信息, 并放在 req 对象中

app

.use((req, res, next) => {

req.user = users[req.session.openId]

next()

})

 

然后实现 /user/info 接口, 用来返回用户信息

 

app

.get('/user/info', (req, res) => {

if (req.user) {

return res.send({

code: 0,

data: req.user

})

}

throw new Error('用户未登录')

})

 

小程序调用用户信息接口

 

getUserInfo () {

return http.get('/user/info').then(response => {

let data = response.data

if (data && typeof data === 'object') {

 

// 获取用户信息成功则保存到全局

this.globalData.userInfo = data

return data

}

return Promise.reject(response)

})

}

 

专为小程序发请求设计的库

小程序代码通过 http.get, http.post 这样的 api 来发请求, 背后使用了一个请求库,@chunpu/http1是一个专门为小程序设计的 http 请求库, 可以在小程序上像 axios 一样发请求, 支持拦截器等强大功能, 甚至比 axios 更顺手

初始化方法如下

import http from '@chunpu/http'

http.init({

baseURL: 'http://localhost:9999', // 定义 baseURL, 用于本地测试

wx // 标记是微信小程序用

})

 

具体使用方法可参照文档 https://github.com/chunpu/http#readme

自定义登录态持久化

浏览器有 cookie, 然而小程序没有 cookie, 那怎么模仿出像网页这样的登录态呢?

这里要用到小程序自己的持久化接口, 也就是 setStorage 和 getStorage

为了方便各端共用接口, 或者直接复用 web 接口, 我们自行实现一个简单的读 cookie 和种 cookie 的逻辑

先是要根依据返回的 http response headers 来种上 cookie, 此处我们用到了 @chunpu/http 中的response 拦截器, 和 axios 用法一样

http.interceptors.response.use(response => {

// 种 cookie

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(() => {}).then(res => {

res = res || {}

var allCookies = res.data || {}

Object.assign(allCookies, cookies)

return util.promisify(wx.setStorage)({

key: 'cookie',

data: allCookies

})

}).then(() => {

return response

})

}

return response

})

当然我们还需要在发请求的时候带上所有 cookie, 此处用的是 request 拦截器

http.interceptors.request.use(config => {

// 给请求带上 cookie

return util.promisify(wx.getStorage)({

key: 'cookie'

}).catch(() => {}).then(res => {

if (res && res.data) {

Object.assign(config.headers, {

Cookie: http.qs.stringify(res.data, ';', '=')

})

}

return config

})

})

 

登录态的有效期

我们知道, 浏览器里面的登录态 cookie 是有失效时间的, 比如一天, 七天, 或者一个月,也许有朋友会提出疑问, 直接用 storage 的话, 小程序的登录态有效期怎么办?

小程序已经帮我们实现好了 session 有效期的判断 wx.checkSession

它比 cookie 更智能, 官方文档描述如下

通过 wx.login 接口获得的用户登录态拥有一定的时效性。用户越久未使用小程序,用户登录态越有可能失效。反之如果用户一直在使用小程序,则用户登录态一直保持有效

也就是说小程序还会帮我们自动 renew 咱们的登录态, 简直是人工智能 cookie, 点个赞�

那具体在前端怎么操作呢? 代码写在 app.js 中

onLaunch: function () {

util.promisify(wx.checkSession)().then(() => {

console.log('session 生效')

return this.getUserInfo()

}).then(userInfo => {

console.log('登录成功', userInfo)

}).catch(err => {

console.log('自动登录失败, 重新登录', err)

return this.login()

}).catch(err => {

console.log('手动登录失败', err)

})

}

 

要注意, 这里的 session 不仅是前端的登录态, 也是后端 session_key 的有效期, 前端登录态失效了,那后端也失效了需要更新 session_key。理论上小程序也可以自定义登录失效时间策略, 但这样的话我们需要考虑开发者自己的失效时间和小程序接口服务的失效时间, 还不如保持统一来的简单。确保每个 Page 都能获取到 userInfo,如果在新建小程序项目中选择 建立普通快速启动模板,我们会得到一个可以直接运行的模板,点开代码一看, 大部分代码都在处理 userInfo....

彻底搞懂微信小程序登录流程-附小程序和服务端代码_第2张图片

注释里写着

由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回,所以此处加入 callback 以防止这种情况。

但这样的模板并不科学, 这样仅仅是考虑了首页需要用户信息的情况, 如果扫码进入的页面也需要用户信息呢? 还有直接进入跳转的未支付页活动页等...

如果每个页面都这样判断一遍是否加载完用户信息, 代码显得过于冗余

此时我们想到了 jQuery 的 ready 函数 $(function), 只要 document ready 了, 就可以直接执行函数里面的代码, 如果 document 还没 ready, 就等到 ready 后执行代码

就这个思路了! 我们把小程序的 App 当成网页的 document

我们的目标是可以这样在 Page 中不会出错的获取 userInfo

Page({

data: {

userInfo: null

},

onLoad: function () {

app.ready(() => {

this.setData({

userInfo: app.globalData.userInfo

})

})

}

})

 

此处我们使用 min-ready2 来实现此功能,代码实现依然写在 app.js 中

import Ready from 'min-ready'

const ready = Ready()

App({

getUserInfo () {

// 获取用户信息作为全局方法

return http.get('/user/info').then(response => {

let data = response.data

if (data && typeof data === 'object') {

this.globalData.userInfo = data

// 获取 userInfo 成功的时机就是 app ready 的时机

ready.open()

return data

}

return Promise.reject(response)

})

},

ready (func) {

// 把函数放入队列中

ready.queue(func)

}

})

 

绑定用户信息和手机号

仅仅获取用户的 openId 是远远不够的, openId 只能标记用户, 连用户的昵称和头像都拿不到。如何获取这些用户信息然后存到后端数据库中呢?

我们在服务端实现这两个接口, 绑定用户信息, 绑定用户手机号

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('用户未登录')

})

.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('用户未登录')

})

小程序个人中心 wxml 实现如下

wx:if='{{!userInfo.nickName}}'

type='primary'

open-type='getUserInfo'

bindgetuserinfo='bindUserInfo'> 获取头像昵称

{{userInfo.nickName}}

wx:if='{{!userInfo.phoneNumber}}'

type='primary'

style='margin-top: 20px;'

open-type='getPhoneNumber'

bindgetphonenumber='bindPhoneNumber'> 绑定手机号

{{userInfo.phoneNumber}}

小程序中的 bindUserInfo 和 bindPhoneNumber 函数, 根据微信最新的策略, 这俩操作都需要用户点击按钮统一授权才能触发

bindUserInfo (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

})

})

})

}

},

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

})

})

})

}

}

转载声明:

本文纯属转载,因个人学习使用,不做任何商业用途!

原文参考地址:http://www.vdongchina.com/read/id/2827.html

你可能感兴趣的:(微信小程序开发)