小程序登录流程 - 连接小程序和服务器代码
用户登录是最完整的应用程序的必要过程
一个简单的用户系统至少要注意这几个方面
- 安全(加密)
- 持久性登录状态(类似cookie)
- 登录过期处理
- 确保用户唯一性,避免多个账户
- 授权
- 绑定用户名、头像等真实手机信息 B❝ 密码保护方式)
很多业务需求可以抽象为Restful接口来配合CRUD操作
但是登录流程比较复杂,各个平台都有自己的流程。事实上,它已经成为项目中耗时的部分,就像小程序一样。登录流程
对于一个从零开始的项目来说,正确的登录流程是一个好的开始,好的开始是成功的一半
本文以微信小程序为平台,全面定制用户登录流程,我们来一起啃这块难啃的骨头
术语解释
首先简单解释一下登录过程时序图中出现的概念
代码
临时凭证,有效期五年分钟,获取会话密钥Session_key
会session_key通过wx.login(),服务器将获得'openid唯一用户名'openid'从未更改过,服务器将获得,服务器将获得通过二维码(公众号、小程序、网站、手机应用)获取微信开放平台同一账号下用户的unionId
。永远不会改变appId
小程序唯一标识appSecret
可换取session_key的code和appId的小程序app Secretnew rawData不含敏感信息的原始数据串,用于计算签名encryptedData
包含敏感信息的用户信息已加密用户签名未被伪造 ” iv
初始加密算法向量
哪些信息是敏感信息?手机号、openId、unionId,可见这些值可以唯一定位用户,而无法找到用户的昵称、头像等不属于敏感信息
与登录相关的功能小程序
wx.login
wx.getUserInfo
wx.checkSession
Promise
小程序
我们发现小程序的异步接口是成功和失败回调。写起来很容易回调地狱
所以我们可以简单地实现一个异步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授权登录
oau代码交换流程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授权
服务端实现上面的‶/
这个接口
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获取对应的用户信息,并把请求V对象中
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('用户未登录')
})
复制代码
Applet调用用户信息接口专门为小程序发送而设计的库requests
小程序 代码通过 http.get
、http.post
等 API 发送请求,背后使用 requests 库。
@chunpu/http 是专门为小程序设计的http。 request库,可以输入小程序的请求,比如axios,支持拦截器等强大功能,甚至比axios还方便
初始化方法如下
import http from '@chunpu/http'
http.init({
baseURL: 'http://localhost:9999', // 定义 baseURL, 用于本地测试
wx // 标记是微信小程序用
})
复制代码
具体使用请参考github文档。 com/chunpu/http…
自定义登录状态持久化
浏览器有cookie,但小程序没有cookie,那么如何模仿网页的登录状态?
这里使用的是自定义小程序持久化接口,即setStorage和getStorage
为了方便各方共享接口或者直接复用Web接口,我们引入一个简单的我们自己读取cookie和保存cookie的逻辑
首先,我们必须根据http响应返回的标头,文件是种子cookie。这里我们使用@chunpu/http
中的响应拦截器,与使用axios是一样的。当然,我们在输入请求时也需要带上所有cookie。这就是请求拦截器被使用的地方。
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已经过期。比如一天、七天或者一个月
有的朋友可能会问。如果我们直接使用存储,小程序的登录状态有效期该怎么办?
对了!小程序帮助我们实现了这次会议。有效性时间评估wx.checkSession
比cookies更智能。官方文档是这样描述的
通过wx.login接口获取用户的登录状态具有一定的时效性。用户不使用小程序的时间越长,用户登录状态失败的可能性就越大。另一方面,如果用户使用了小程序,用户的登录状态将一直保持有效
也就是说,小程序也会帮助我们自动重置我们的登录状态,这只是一款人工智能饼干,你喜欢吗?
如何具体控制前端?代码写在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
理论上小程序也可以自定义登录过期时间策略,但这种情况下,我们需要考虑开发者自身的过期时间和小程序接口服务过期的时间。最好是统一的。简单
确保每个页面都能获取用户信息
如果您选择 在新的小程序项目中创建通用的快速启动模板
,我们得到一个可以直接运行的模板
open代码Na一看,大部分代码都是处理userInfo的....![小程序登录流程-附小程序和服务端代码]()
注释说
因为getUserInfo是网络请求,在Page.onLoad之后可能会返回
,所以这里加了一个回调以避免这。这种情况
但是这样的模板并不科学。它只考虑主页需要用户信息的情况。如果扫码进入的页面也需要用户信息怎么办?还有直接跳转到未付费页面活动的页面等等...
如果每个页面都这样评估用户信息是否已经加载,代码就显得太冗余了
此时我们想jQuery准备好的函数$(function)
,如果文档准备好了,就可以直接运行函数中的代码了。如果文档还没准备好,就等文档准备好了再运行代码
这是一个想法!我们把小程序应用视为一个网站文档
我们的目标是获取页面上的userInfo没有错误的页面
Page({
data: {
userInfo: null
},
onLoad: function () {
app.ready(() => {
this.setData({
userInfo: app.globalData.userInfo
})
})
}
})
复制代码
这里实现这个功能我们使用min-ready
代码实现还是写在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小程序个人中心实现如下
<view wx:if="userInfo" class="userinfo">
<button
wx:if="{{!userInfo.nickName}}"
type="primary"
open-type="getUserInfo"
bindgetuserinfo="bindUserInfo"> 获取头像昵称 </button>
<block wx:else>
<image class="userinfo-avatar" 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"> 绑定手机号 </button>
<text wx:else>{{userInfo.phoneNumber}}</text>
</view>
复制代码
小程序中的函数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
})
})
})
}
}
复制代码
代码
本文提到的代码可以在我的github上找到
小程序代码在wxapp-login -demo
服务端代码Node.js在wxapp-login -server
作者:七物周刊
链接:https://juejin.im/post/5bda95a22223f来源:掘金
版权归作者所有。商业转载请联系作者获得许可。商业请注明来源。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。