Egg.js+MongoDB開發小程式介面 (三) 與微信伺服器互動及使用者資訊加解密

蘿蔔依依的爸發表於2020-11-11

什麼是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,
        },
        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);
...
};

相關文章