Node.js +小程序登录详解(新)
前言
此篇内容记录本人编程过程中所遇到的一些问题和解决的办法。仅供参考,有些内容引用别处。[非商用,如侵删]
正文
先看图 基于小程序官方给出的图片 经过网上查找 到一张更加详细的时序表
根据疑惑我把这张图中的内容拆分成一个个小的问题来解决
第一步:通过code获取openid和session_key
微信小程序端 App.js wx.login引用官方文档的示例
1)、获取code、iv、encryptedData
iv和encryptedData后面用来解密用户信息
// 小程序全局只触发一次 触发时机为初始化后执行的生命周期函数,
onLaunch(){
wx.getUserProfile({
lang: 'zh_CN',
desc: 'desc',
success(res) {
const {encryptedData,iv} = res
wx.login({
success(res) {
wx.request({
url: 'http://localhost:3000/api/user/sendcode',
data: {
code: res.code,
iv,
encryptedData,
}
})
}
})
}
})
}
调用wx.login()方法 将获取到的 res.code 通过 wx.request() 发送到后端 接收到数据后把数据保存起来
iv和encryptedData
2)、获取session_Key和openid
后端携带 Appid、AppSecret、code向微信服务器发起GET请求
const { iv, code, encryptedData } = req.query// 解构赋值
// 向微信服务器发起get请求 通过 code 换取 Session_key 和 openid
let wxURl = `https://api.weixin.qq.com/sns/jscode2session?
appid=${wxAppkey.appid}
&secret=${wxAppkey.secret}
&js_code=${code}
&grant_type=authorization_code`
// 保存请求结果
const { data } = await $.get(wxURl)
至此 我们已经拿到了 session_key 和 openid 的值
3)、解密获取用户信息
官方有给出demo > 开放数据校验与解密
node 实例
WXBizDataCrypt.js
var crypto = require('crypto')
function WXBizDataCrypt(appId, sessionKey) {
this.appId = appId
this.sessionKey = sessionKey
}
WXBizDataCrypt.prototype.decryptData = function (encryptedData, iv) {
//转换为base64格式数据
iv = Buffer.from(iv, 'base64')
encryptedData = Buffer.from(encryptedData, 'base64')
sessionKey = Buffer.from(this.sessionKey, 'base64')
try {
//根据给定的算法,密钥和初始化向量,创建并返回一个Decipher解密对象。
var decipher = crypto.createDecipheriv('aes-128-cbc', sessionKey, iv)
// 设置自动 padding 为 true,删除填充补位
decipher.setAutoPadding(true)
//往decipher实例中添加数据,第一个参数是数据,第二个参数是传入数据的格式,默认是 ‘binary’。第三个参数是返回的数据格式。
var decoded = decipher.update(encryptedData, 'binary', 'utf8')
//返回任何剩余的解密内容。不能调用多次
decoded += decipher.final('utf8')
decoded = JSON.parse(decoded)
} catch (err) {
throw new Error('Illegal Buffer')
}
//检验是否获取正确,是当前appid的数据
if (decoded.watermark.appid !== this.appId) {
throw new Error('Illegal Buffer')
}
return decoded
}
module.exports = WXBizDataCrypt
puzzle.js
const WXBizDataCrypt = require('./WXBizDataCrypt')
const { wxAppkey } = require('../../config/wxappConfig')
function getPuzzleUserinfo(sessionKey, encryptedData, iv) {
// 创建实例将appid 和session_key传入原型中
const pc = new WXBizDataCrypt(wxAppkey.appid, sessionKey)
// 将传入的iv,encryptedData,和实例原型中的session_key 一起进行解密 结果返回给data
const data = pc.decryptData(encryptedData, iv)
return data
}
module.exports = {
getPuzzleUserinfo
}
*了解:什么是3rd_session?(自定义登录态)
什么是登陆态:
抱着这个疑惑我去网络上找各种答案,零零散散的看了很多:
所谓登录态,就是程序在运行时,能够识别当前用户,能够证明自己的唯一性且合法。
WEB服务器通过浏览器携带的token获取session来判断是否是同一用户(或浏览器);Restful服务通过客户端传过来唯一ID,来识别调用用户
以上内容引自微信小程序登录态
希望帮助理解,我的理解:
登录态是用来校验用户在小程序端和第三方服务器端的登录状态,类似与传统Web项目中的token和sseion。
什么是自定义登录态?
我的理解就是是自己生成的一个字符串 用于验证用户登录态的伪token然后我们需要为其设定失效时间并保管在缓存(我这里用的redis)中,用户每次发起请求都要携带这个伪token,然后我们判断Token是否过期决定能否请求成功,如果未过期,返回相关的数据,如果过期了则需要用户重新发起登录请求重新返回给其token值。
第二步:生成自定义登录态并传入缓存
先继续上码🐎
user.js
if (req.method === 'GET' && req.path === '/api/user/sendcode') {
// 向服务器发起请求获取到session_key和openid以及解密后的用户信息userinfo 解构赋值得到 (sessionKey/openid)、SQL数据模型、Res数据模型
let { data: wxOpenidAndKey, userinfoSQLData, userinfoResData } = await getWxokAndUserinfo(req)
// 对合并后的内容wxos进行加密
let { result: token, wxos: value } = sha1(wxOpenidAndKey)
// 以Token为 Key wosx为值 存入缓存
redis.setRedis(token,wxos)
}
----------上面用到的方法---------------------------
//getWxOpenidAndKey()
async function getWxokAndUserinfo(req, res) {
const { iv, code, encryptedData } = req.query// 解构赋值
// 向微信服务器发起get请求 通过 code 换取 Session_key 和 openid
let wxURl = `https://api.weixin.qq.com/sns/jscode2session?appid=${wxAppkey.appid}&secret=${wxAppkey.secret}&js_code=${code}&grant_type=authorization_code`
// 保存请求结果
const { data } = await $.get(wxURl)
// 如果为空,将错误抛出
if (!data) throw '向微信服务器发起get请求错误'
// 解构赋值出session_key和openid
const { session_key, openid } = data
// 通过对传回session_key、iv、encryptedData进行解密获取用户的信息
const userinfo = getPuzzleUserinfo(session_key, encryptedData, iv)
// 将内容openid和用户信息userinfo封装成一个数据模型 准备传回数据库
const userinfoSQLData = userinfoSQLModels(userinfo, openid)
const userinfoResData = userinfoResModels(userinfo)
1)、生成自定义登录态
根据官方建议是通过Linux 下的’/dev/urandom’, ‘rb’ 生成一个随机数来作为key。
但是!因为我对Linux并不了解也没有Linux的设备
所以我用了nodejs中的crpto加密模块中的加盐算法,用一个11位的随机数作为Salt 对session_key+openid进行加密,作为 伪Token。
先上🐎
// 加密 生成的随机字符
let { result: token, wxos: value } = sha1(wxOpenidAndKey)
sha1.js
const crypto = require('crypto')
// 随机生成一个11位数的随机数
let randnumber11 = Math.random().toString(36).substr(2)
//封装方法-> 将传入的值(token) 通过加盐算法加密 盐为一个随机生成的11位随机数
function sha1(wxOpenidAndKey) {
const { openid, session_key } = wxOpenidAndKey
// 用问号分割key和id
const wxos = session_key + '?' + openid;
console.log('controller', wxos);
// 通过hamc 加盐算法 将哈希算法sha1 与一个密钥->随机生成的11位随机数作为佐料(盐)加密
let hamc = crypto.createHmac('sha1', randnumber11)
// 加密传入的str并返回64位16进制数的值 作为 token的 key
let result = hamc.update(wxos).digest('hex')
if (!result) {
console.log('加密失败');
}
return {result,wxos};
}
module.exports = {
sha1
}
2)、将自定义登录态写入redis缓存
这里涉及到了 redis 的一些内容
我稍微看了一下文档决定先用在看之后再往深了研究
相关的内容在我的另一篇博客 Node.js: redis 和 ioredis 的基本使用
里面封装了几个基本的使用方法
//添加进缓存 key为token 值为openid 和session_key
redis.setRedis(token,wxos)
然后可以再redis的命令行中 输入
keys *
查询所有的键值对 看看是不是添加成功了
第三步:将token返回给小程序端
这里默认用户的登录状态是首次登录
1)、通过数据模型返回给前端
return new SuccessModel(userinfoResData, token, '登录成功,Token已保存Redis')
// 数据模型
class BaseModel {
constructor(data, token=false,message) {
if ((typeof data) === "string") {
this.message = data;
token = null;
data = null;
message = null;
}
if (data) {
this.data = data
}
if (message) {
this.message = message
}
if (token) {
this.token = token
}
}
}
// 成功时返回的数据模型
class SuccessModel extends BaseModel {
constructor(data,token=false, message) {
super(data, message)
this.success = true;
this.errno = 0;
this.token = token;
}
}
第四步:小程序端保存 token
小程序端接收到返回的数据后将token保存至Storage
//key为token 值为发起请求返回的token
wx.setStorageSync('Token', res.token)
第五步:封装wx.request() 让小程序端携带 token发起请求
我这里对wx.request()方法进行了封装
1、先转化为promise形式请求
2、再转为通过async/await简化的方式请求
将storage中的token 携带再请求头里 这样每次就都能携带token发起请求了
// wx.request() 网络请求转换Promise回调形式
function wxRequest (method, url, data) {
// 将request请求转化为Promise类型的请求
return new Promise((reslove,reject)=>{
const token = wx.getStorageSync('Token')
wx.request({
url: url,
method: method,
data: data,
header: {
'content-type': method == 'GET' ? 'application/json' : 'application/x-www-form-urlencoded',
'Accept': 'application/json',
'token':token
},
dataType: 'json',
success: function (res) {
reslove(res.data);
},
fail: function (err) {
reject(err);
}
})
})
}
// Promise回调形式请求转化为 async/await 请求
async function WxRequestSync(method, url, data) {
const result = await wxRequest(method,url,data)
return result
}
module.exports = {
wxRequest,
WxRequestSync
}
第六步:服务端收到 token分析用户登录形态
1)、通过封装的redis查询方法判断token是否过期
获取请求头中的token 如果是第一次登录 则为空
----
const { token: wxToken } = req.headers
如果请求头中的token 不为空 就在Redis中查询 是否有key 如果结果为0 那么说明该Token已经过期
---
const isRedisToken = await redis.queryRedis(wxToken)
① token过期
如果结果返回 0 那么说明token过期
1、需要重新拿用户传入的code 去向微信服务器交换session_key 和openid
let { data: wxOpenidAndKey } = await getWxokAndUserinfo(req)
2、拿到加密后的token 和 value(openid和session_key)
// 对合并后的内容wxos进行加密
let { result: token, wxos: value } = sha1(wxOpenidAndKey)
3、保存进redis、通过openid查询数据库中的数据 并返回
// 将token 和value(openid和session)存进redis
let isSetRedis = await redis.setRedis(token, value)
// 判断是否存入成功 成功的话将token值传给前端保存在Storage中
if (isSetRedis != 'OK') throw isSetRedis;
//拿到openid
let reidsOpenid = await (await redis.getRedis(token)).split('?')[1]
let res = await queryUserinfo(reidsOpenid)
return new SuccessModel(res, token)
② token未过期
如果结果返回 1 那么说明token未过期
执行下面的操作
1、为该 token 增加过期时间(自定)
2、查询到该token对应的值 拿到openid
3、在数据库中查询 该openid 的数据
4、 通过成功的数据模型 返回给前端
let redisToken = await (await redis.getRedis(wxToken)).split('?')[1]
timeSetRedis('wxToken',3600) //增加过期时间 6个小时
let res = await queryUserinfo(redisToken)
if (res != '') {
return new SuccessModel(res)
} else {
return new ErrorModel(res)
}
完结撒花 此篇内容有不对的地方欢迎大佬批评指正
如果有不理解的地方也可以评论区评论一下~