什么是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;

#与小程序交互

创建用户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);
...
};