深入了解微信小程序登录流程—附带小程序和服务端代码_微信服务器代码是什么
深入了解微信小程序登录流程—附带小程序和服务端代码
CSDN 标签: 微信小程序, 登录流程, 前端开发, 后端开发, Express.js, 小程序开发, OAuth, 用户认证, 小程序接口
文章介绍
用户登录是大多数完整应用程序的必要步骤。对于一个简单的用户系统,需要关注以下几个方面:
-
安全性(加密)
-
持久化登录态(类似 cookie)
-
登录过期处理
-
确保用户唯一性,避免出现多账号
-
授权
-
绑定用户昵称、头像等信息
-
绑定手机号(实名与密保方式)
许多业务需求可以抽象成 RESTful 接口,配合 CRUD 操作。但是,登录流程往往复杂,因为不同平台的流程各不相同,因此,开发时可能会消耗大量时间。本文将以微信小程序为例,讲解一个完整的自定义用户登录流程,帮助开发者顺利应对这块难啃的“骨头”。
名词解释
在登录流程的时序图中,以下是一些常见名词的简要说明:
-
code:临时登录凭证,有效期为五分钟,可以通过
wx.login()获取。 -
session_key:会话密钥,通过服务端使用
code2Session获取。 -
openId:用户在该小程序下的唯一标识,始终不变,服务端通过
code获取。 -
unionId:用户在同一微信开放平台帐号(公众号、小程序、网站、移动应用)下的唯一标识,始终不变。
-
appId:小程序的唯一标识。
-
appSecret:小程序的密钥,可以与
code和appId一起交换session_key。
其他名词
-
rawData:不包含敏感信息的原始数据字符串,用于计算签名。
-
encryptedData:包含敏感信息的用户数据,是加密后的。
-
signature:用于校验用户信息是否被篡改。
-
iv:加密算法的初始向量。
敏感信息:手机号、openId、unionId。由于这些值能唯一标识用户,所以是敏感信息。相对而言,昵称、头像等不具备唯一标识功能的则不算敏感信息。
小程序登录相关函数
-
wx.login -
wx.getUserInfo -
wx.checkSession
小程序的 Promise
微信小程序的异步接口大多采用 success 和 fail 回调,容易引发回调地狱。我们可以通过将异步接口转化为 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 });
服务端实现
此部分的服务端实现基于 Express.js。为了简化演示,服务端使用 JavaScript 变量存储用户数据,意味着服务端重启时用户数据会丢失。若需要持久化存储数据,可通过数据库实现。
const users = { openId: { openId: \'\', sessionKey: \'\', nickName: \'\', avatarUrl: \'\', unionId: \'\', phoneNumber: \'\' } }; app.use(bodyParser.json()) .use(session({ secret: \'alittlegirl\', resave: false, saveUninitialized: true }));
小程序登录
以下是前端小程序中实现登录的方法。我们首先实现一个 OAuth 授权登录,代码将 code 换取 openId 和 sessionKey。
login () { console.log(\'登录\'); return util.promisify(wx.login)().then(({code}) => { console.log(`code: ${code}`); return http.post(\'/oauth/login\', {code, type: \'wxapp\'}); }); }
服务端实现 /oauth/login 接口:
auth/login\', (req, res) => { var {code, type} = req.body; if (type === \'wxapp\') { 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 可以返回用户的基本信息(如昵称、头像等)。若用户未登录,则返回\"用户未登录\"。
服务端中,我们通过 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(\'用户未登录\'); });
小程序调用用户信息接口
小程序中通过调用 http.get(\'/user/info\') 来获取用户信息:
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); }); }
自定义登录态持久化
小程序没有 cookie,但可以通过 setStorage 和 getStorage 来模拟登录态的持久化。为了方便共用接口,我们实现了一个简单的读写 cookie 逻辑:
http.interceptors.response.use(response => { 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; });
登录态的有效期
微信小程序通过 wx.checkSession 来判断登录态的有效性,且小程序会自动帮我们更新过期的登录态。具体实现如下:
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); }); }
确保每个页面都能获取到用户信息
为了确保每个页面都能获取到用户信息,我们可以用类似 jQuery 的 ready 函数来等待 userInfo 加载完毕再执行相关逻辑。
代码如下:
Page({ data: { userInfo: null }, onLoad: function () { app.ready(() => { this.setData({ userInfo: app.globalData.userInfo }); }); } });
通过这种方式,每个页面都能顺利获取到用户信息,避免了冗余的判断。
绑定用户信息和手机号
绑定用户信息和手机号的过程需要用户授权,并且服务端通过微信的 API 解密数据,保存到数据库中。
服务端代码:
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(\'用户未登录\'); }); app.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(\'用户未登录\'); });
小程序个人中心页面实现
<button wx:if=\"{{!userInfo.nickName}}\" type=\"primary\" open-type=\"getUserInfo\" bindgetuserinfo=\"bindUserInfo\"> 获取头像昵称 <image class=\"userinfo-avatar\" src=\"{{userInfo.avatarUrl}}\" mode=\"cover\"> {{userInfo.nickName}} <button wx:if=\"{{!userInfo.phoneNumber}}\" type=\"primary\" style=\"margin-top: 20px;\" open-type=\"getPhoneNumber\" bindgetphonenumber=\"bindPhoneNumber\"> 绑定手机号 {{userInfo.phoneNumber}}
绑定用户信息和手机号的函数
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 }); }); }); } }


