什么是Token
1、Token的引入:Token是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下,Token便应运而生。
2、Token的定义:Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。
3、使用Token的目的:Token的目的是为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。
#与微信服务器交互
##获取access_token
获取小程序全局唯一后台接口调用凭据(
access_token
)。调用绝大多数后台接口时都需使用 access_token,开发者需要进行妥善保存。
请求地址
GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
access_token 有请求频率和次数限制,但他有较长的时效性,所以我们可以将其储存在MongoDB并挂载到Egg的Application上,方便在做其他请求时调用
我们利用MongoDB的TTL 和Egg的定时任务 来让Application上挂载的Token 永不过期
service
app/service/wxminiprogram/index.js
用于获取小程序开发所需的appid 和密钥
'use strict';
const Service = require('egg').Service;
class wxmpIndexService extends Service {
constructor(ctx) {
super(ctx);
this.WXAuth = this.config.wxmpCf;
}
}
module.exports = wxmpIndexService;
app/service/wxminiprogram/auth.js
获取 储存 微信小程序access_token
'use strict'
const Service = require('egg').Service;
//
class wxmpAuth extends Service {
constructor(ctx) {
super(ctx);
this.WXAuth = this.config.wxmpCf;
this.MODEL = ctx.model.Auth.Token
}
//检查Token 是否存在 给Egg定时任务用
async checkToken(type = 1) {
const ctx = this.ctx;
try {
const MR = await this.MODEL.findOne({
type
});
if (!!MR && !ctx.app.wxToken) {
const access_token = await this.getToken();
ctx.app.wxToken = access_token;
}
if (!MR) {
const RESULT = await this.MPAuth();
ctx.app.wxToken = RESULT.access_token;
}
} catch (error) {
console.log(error)
}
}
//获取系统中Token
async getToken(type = 1) {
try {
const MB = await this.MODEL.findOne({
type
});
return !MB ? (await this.MPAuth()).access_token : MB.access_token
} catch (error) {
console.log(error);
return error
}
}
async MPAuth() {
try {
const RESULT = await this.WXMPToken();
const MR = await this.MODEL.findOneAndUpdate({
type: 1
}, {
endTime: new Date(),
...RESULT.data
}, {
upsert: true
});
return RESULT.data
} catch (error) {
console.log(error)
return error
}
}
// 从微信服务器获取access_token
WXMPToken() {
const ctx = this.ctx;
const {
appid,
secret
} = this.WXAuth;
return ctx.curl('https://api.weixin.qq.com/cgi-bin/token', {
dataType: 'json',
data: {
grant_type: 'client_credential',
secret,
appid,
type: 1
}
});
}
}
module.exports = wxmpAuth;
model
app/model/auth.js
module.exports = app => {
const mongoose = app.mongoose;
const Schema = mongoose.Schema;
const conn = app.mongooseDB.get('mp');
const tokenSchema = new Schema({
// TTL 过期 默认60*20
// token
access_token: {
type: String
},
// token 類型 我们在实际的项目开发中肯定不只使用腾讯的token 还会使用其他第三方的 所有这里给token 分类方便刷新更替 {1:腾讯小程序token}
type: {
type: Number,
},
// 提前10分钟删除token(MongoDB的TTL机制是每分钟检查一次并清除,遇到处理不过来就会延迟 所以提前10分钟 清理)
endTime: {
type: Date,
default: Date.now,
index: {
expires: 6600
}
},
expires_in: {},
// 有效期
updateTime: {
type: Date
}
}, {
timestamps: {
createdAt: 'created',
updatedAt: 'updated'
}
});
tokenSchema.statics = {
addOne: async function(body) {
try {
return await this.model.create({...body });
} catch (error) {
return error
}
}
}
return conn.model('third_token', tokenSchema);
}
schedule 定时任务
app/schedule/token_refresh.js
const Subscription = require('egg').Subscription;
class refresh_token extends Subscription {
constructor(ctx) {
super(ctx);
this.wxAuthService = ctx.service.wxminiprogram.auth;
}
static get schedule() {
// 每10s执行一次token检查 项目启动时执行一次将未过期的token挂载到Application(开发时可以设置为此,开始时可能会不断的刷新热更代码)
return {
interval: '10s',
type: 'worker',
immediate: true,
env: 'local'
};
}
async subscribe() {
const ctx = this.ctx;
try {
await this.wxAuthService.checkToken();
} catch (error) {
console.log(error)
return error;
}
}
}
module.exports = refresh_token;
#与小程序交互
app/service/account/jwt.js
'use strict';
const Service = require('egg').Service;
class JwtService extends Service {
create(OBJ) {
const { app } = this
// const key = app.config.keys.replace(/_/g, '');
return app.jwt.sign({...OBJ }, app.config.jwt.secret, { algorithm: 'HS256' });
}
}
module.exports = JwtService;
##微信小程序用户加解密小程序
###加解密函数
app/lib/WXBizDataCrypt.js
var crypto = require('crypto')
function WXBizDataCrypt(appId, sessionKey) {
this.appId = appId
this.sessionKey = sessionKey
}
WXBizDataCrypt.prototype.decryptData = function(encryptedData, iv) {
// base64 decode
var sessionKey = new Buffer(this.sessionKey, 'base64')
encryptedData = new Buffer(encryptedData, 'base64')
iv = new Buffer(iv, 'base64')
try {
// 解密
var decipher = crypto.createDecipheriv('aes-128-cbc', sessionKey, iv)
// 设置自动 padding 为 true,删除填充补位
decipher.setAutoPadding(true)
var decoded = decipher.update(encryptedData, 'binary', 'utf8')
decoded += decipher.final('utf8')
decoded = JSON.parse(decoded)
} catch (err) {
throw new Error('Illegal Buffer')
}
if (decoded.watermark.appid !== this.appId) {
throw new Error('Illegal Buffer')
}
return decoded
}
module.exports = WXBizDataCrypt
##获取小程序用户加密信息Service
app/service/wxminiprogram/user.js
'use strict'
const indexService = require('./index');
const WXBizDataCrypt = require('../../libs/WXBizDataCrypt');
//
//
class weChatTemplateService extends indexService {
constructor(ctx) {
super(ctx);
this.userMODEL = ctx.model.Account.User;
this.tokenSERVICE = ctx.service.account.jwt;
this.BASICUSER = ctx.model.Basics.User.User;
};
async createBasicUser(body, phone) {
const RBD = await this.BASICUSER.findOneAndUpdate({ phone }, body, { new: true, upsert: true });
};
// 解密数据
async descDatas(DATAS, reg = false) {
const { encryptedData = null, iv = null, js_code = null } = DATAS;
const { appid } = this.WXAuth;
try {
if (!!js_code) {
if (!!encryptedData) {
const { session_key, openId } = await this.getOpenID(js_code, (iv && !!reg) ? true : false, true);
const WXBDC = new WXBizDataCrypt(appid, session_key);
const RESULT = WXBDC.decryptData(encryptedData, iv);
return RESULT
}
}
} catch (error) {
console.log(error)
return { error: '服务器忙,请重试!', code: 500 }
}
};
// 绑定手机
async bindPhone(DATAS, _id) {
let RESULT;
try {
RESULT = await this.descDatas(DATAS);
if (!!RESULT.error) { throw RESULT.error; return };
const { phoneNumber, countryCode } = RESULT;
const RBD = await this.userMODEL.findOneAndUpdate({ _id }, { 'telphone': phoneNumber }, { new: true });
const { telphone: phone = null, wxUserInfo: wx_UserInfo, unionid: wx_unionid } = RBD;
!!phone && await this.createBasicUser({ phone, wx_UserInfo, wx_unionid }, phone);
return { phone }
} catch (error) {
return { message: error, data: {}, code: 204 }
}
};
// 获取步数
async getRun(DATAS) {
const RESULT = await this.descDatas(DATAS);
return RESULT;
};
async getOpenID(js_code, regUser, onlyId = false) {
const ctx = this.ctx
const { appid, secret } = this.WXAuth;
try {
const RESULT = await ctx.curl('https://api.weixin.qq.com/sns/jscode2session', {
dataType: 'json',
data: {
grant_type: 'authorization_code',
secret,
appid,
js_code
}
});
const { openid: openId = null, session_key = null, unionid = null, errcode } = RESULT.data;
if (!!onlyId) return { session_key };
if (!onlyId && !!session_key) {
const MR = await this.userMODEL.findOneAndUpdate({ openId }, { openId, unionid }, { 'upsert': true, 'new': true });
if (!regUser) {
const { _id: uid, isWXAuth, openId, wxUserInfo: userInfo, telphone = null, _merchant, _merchant: { _id: mid = null, role = null } } = MR;
// console.log(MR)
return this.setToken({ uid, isWXAuth, openId, mid, role }, { isWXAuth, 'userInfo': {...userInfo, telphone, uid, _merchant } });
} else {
return { session_key, openId }
}
} else {
return { code: 401, message: '获取用户信息失败' }
};
} catch (error) {
// console.log(error)
return error
}
};
setToken(params, other) {
return { 'token': this.tokenSERVICE.create(params), ...other }
};
async getUserInfo(DATAS) {
const ctx = this.ctx;
const { encryptedData = null, iv = null, js_code = null } = DATAS;
const { appid } = this.WXAuth;
try {
if (!!js_code) {
if (!!encryptedData && !!iv) {
const BBBB = await this.getOpenID(js_code, !!iv);
const { session_key, openId } = BBBB;
const WXBDC = new WXBizDataCrypt(appid, session_key);
const RESULT = WXBDC.decryptData(encryptedData, iv);
if (!!RESULT.openId) {
delete RESULT.openId;
delete RESULT.watermark;
// 注册用户
const MB = await this.userMODEL.findOneAndUpdate({ openId }, { isWXAuth: true, wxUserInfo: RESULT }, { upsert: true, new: true });
const { _id: uid, isWXAuth, wxUserInfo: userInfo, telphone = null, _merchant = null, _merchant: { _id: mid = null } } = MB;
// 初始化收藏
// await ctx.model.Account.Collect.initFn(uid);
// 返回数据
return this.setToken({ uid, isWXAuth, openId, mid }, { isWXAuth, 'userInfo': {...userInfo, telphone, uid, _merchant } });
}
} else {
return await this.getOpenID(js_code);
}
}
} catch (error) {
console.log(error)
return error
}
}
}
module.exports = weChatTemplateService;
##控制器
app/controller/mp/account/user.js
'use strict';
const Controller = require('../../index');
class UserController extends Controller {
constructor(ctx) {
super(ctx);
this.MODEL = ctx.model.Account.User;
this.SERVICE = ctx.service.wxminiprogram.user
};
// 刷新token
async refreshToken() {
const ctx = this.ctx;
const {
uid: _id
} = ctx;
const {
mid,
openId,
isWXAuth,
userInfo: {
uid
},
userInfo
} = await this.show(null, true, _id);
ctx.body = this.SERVICE.setToken({
uid,
isWXAuth,
openId,
mid
}, {
isWXAuth,
userInfo
})
};
// 获取用户自己数据
async show(e, refresh = false, _uid = null) {
const ctx = this.ctx
const {
uid: _id
} = ctx;
const {
_id: uid,
telphone,
_merchant,
_merchant: {
_id: mid
},
isWXAuth,
openId,
wxUserInfo: {
nickName,
avatarUrl,
gender,
city,
province,
country
}
} = await this.MODEL.findOne({
_id: _id ? _id : _uid
}, 'wxUserInfo isWXAuth telphone _merchant openId');
const BODY = {
...refresh ? {
mid,
openId
} : {},
isWXAuth,
userInfo: {
uid,
telphone,
_merchant,
nickName,
avatarUrl,
gender,
city,
province,
country
}
}
try {
if (!!refresh) {
return BODY
};
ctx.body = BODY
} catch (error) {
console.log(error)
}
};
// 创建用户(临时用户)(jscode:小程序获取jscode 用户数据 encryptedData iv)
async create() {
const ctx = this.ctx
let { jscode: js_code, encryptedData = null, iv = null } = ctx.request.body;
try {
ctx.body = await this.SERVICE.getUserInfo(this.DUFN({ js_code, encryptedData, iv }))
} catch (error) {
console.log(error)
}
// ctx.body = await this.SERVICE.getUserInfo(this.DUFN({ js_code, encryptedData, iv }))
};
// 绑定用户手机
async bindPhone() {
const ctx = this.ctx;
const { uid = null } = ctx;
let { jscode: js_code, encryptedData = null, iv = null } = ctx.request.body;
try {
const RBD = await this.SERVICE.bindPhone(this.DUFN({ js_code, encryptedData, iv }), uid);
ctx.body = RBD
} catch (error) {
console.log('bindPhone_ctrl', error)
}
};
}
module.exports = UserController;
##创建路由
创建用户登录相关router
app/router/mp/account.js
module.exports = app => {
const { router, controller } = app;
/**
* @name 微信用户体系
*/
// 用户登录
router.post('wxmp', '/api/v1/mp/account/user/auth', controller.mp.account.user.create);
// 用户绑定手机
router.post('wxmpBindPhone', '/api/v1/mp/account/user/bindPhone', app.jwt, controller.mp.account.user.bindPhone);
// 获取用户信息
router.get('getUserInfo', '/api/v1/mp/account/user/info', app.jwt, controller.mp.account.user.show);
// 刷新token
router.get('refreshToken', '/api/v1/mp/account/refreshToken', app.jwt, controller.mp.account.user.refreshToken)
}
在主路由注册
app/router.js
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
...
require('./router/mp/account')(app);
...
};