基于Taro框架的微信小程序JWT授权登录方案

背景

小程序这两年可谓是风风火火,已经涉及到了行业的方方面面,作为一枚前端码农,为了紧跟时代的步伐,满足市场的需要,楼主也步入了小程序的坑中,一探究竟,从最初的原生开发,过渡到WePY的引入,又转到了mpvue的框架,最近又准备踩踩Taro的坑 了~

之所以从mpvue框架迁移到Taro框架,一是由于该框架是京东凹凸实验室开发维护的,目前来看比较活跃,更新迭代速度比较快,质量上有一定的保障;二是该框架支持多端统一,一劳永逸,为后面拓展H5等其他业务奠定基础~

因业务需求,我们采用JWT进行身份鉴权,所以才有了这篇JWT授权登录 解(cai)决(keng)方(zhi)案(nan),如果对JWT还不是很了解的小伙伴,请猛戳这里~

ok,不废话了,直接进入你们最想看到代码环节~

正文

关于Taro的安装的使用,我这里就不再赘述了,具体的步骤可以参考官网给出的教程,我们直接来看看授权登录这一块~

FBI WARNING : 本文相关的代码是跟楼主目前公司的有些业务场景有关,如有不同,请自行更改即可~

初始化Taro项目时,我们需要选择使用Redux模版,进行状态管理,然后默默等待依赖安装完成,通过npm run dev:weapp命令就可以启动我们的小程序服务了~

接下来跟着我的步骤走下去~

step1

首先,我们需要使用到Redux定义一些app相关的数据,方便我们后面的判断和使用~

Redux三步走,reducerconstantaction 我这里就直接贴代码了~

  • constants目录
//constants  app.js

//更改app登录状态
export const CHANGE_APP_ON_LAUNCH = 'change_app_on_launch';

//写入授权信息
export const INSERT_AUTHORIZE = 'insert_authorize';

  • actions目录
// actions app.js

import * as constants from '../constants/app'

//更改登录状态
export const changeAppOnLaunch = () => ({
    type : constants.CHANGE_APP_ON_LAUNCH
})

//写入请求token
export const insertToken = (authorize) => ({
    type : constants.INSERT_AUTHORIZE ,
    authorize
})

  • recuders目录
import * as constants from '../constants/app'

const INITIAL_STATE ={
    //请求接口域名地址
    baseURL : 'https://xx.xxx.xx/' ,
    //应用首次加载
    appOnLaunch : true ,
    //请求token
    authorize : ''
}

export default function app( state = INITIAL_STATE , action ){
    switch (action.type){
        case constants.CHANGE_APP_ON_LAUNCH :
            return {
                ...state ,
                appOnLaunch : false
            };
        case constants.INSERT_AUTHORIZE :
            return {
                ...state ,
                authorize : action.authorize
            };
        default :
            return state;
    }
}

最后不要忘记将其通过 combineReducers 方法合并到 Redux 里面去~

为了方便后面的使用,这里我在入口文件 app.js 中将 store 直接挂在到了 Taro 对象上,这样我们也就可以在后面通过Taro.$store这种方式直接获取进行操作处理,即:

 //app.js
 
 componentDidMount() {
    //将redux状态挂载到 Taro 对象上,方便使用
    Taro.$store = store;
}

step2

我们在src创建一个utils文件夹,用来存放我们一些公用的方法之类的文件~

由于微信小程序的限制,我们无法得知用户的登录状态是否过期,所以我们必须要通过wx.checkSession 来接口检测当前用户登录态是否有效,做对应的逻辑处理~

因此,我们创建一个auth.js文件,用来微信登录,获取获取授权信息,其流程可以参考官方的这张时序图!

我们的JWT就是上面图所提到的自定义登录状态,它一般是长这样的

其中exp是令牌过期时间,iat令牌签发时间,nbf令牌生效时间,token是用户状态,包含用户的身份信息,我们主要会用到exptoken 两个字段,用来判断令牌是否过期和发送请求携带用户身份~

因此,我的做法是,在用户进入小程序的时候通过Taro.checkSession(),检查session_key是否过期,如果过期需要通过 Taro.login()重新登录进行授权,获取JWT授权信息,否则就判断用户的令牌是否过期,如果过期就要重新刷新令牌,重新获取,其代码如下~

// auth.js

import Taro from '@tarojs/taro'
import { insertToken , changeAppOnLaunch } from '../actions/app'
//获取数据
export default class Auth {
//app授权
static appCheckAuth(){
    return new Promise(function (resolve) {
        const state = Taro.$store.getState();
        //如果有授权信息
        if( Auth.checkAuth() && !state.app.appOnLaunch ){
            //直接返回
            resolve(true);
        }else{
            //判断session_key是否过期
            Taro.checkSession().then(async ()=>{
                //未过期检查token是否有效
                if( !Auth.checkAuth() ){
                    //判断是否 token 请求成功
                    let flag = await getAuthToken();
                    if( flag ) {
                        //更新app状态
                        Taro.$store.dispatch(changeAppOnLaunch());
                        resolve(true);
                    }else{
                      //提示
                        Taro.showToast({
                        title : '获取授权信息失败' ,
                        icon : 'none' ,
                        mask : true
                        })
                    }
                }else{
                    //更新app状态
                    Taro.$store.dispatch(changeAppOnLaunch());
                    //token 没有过期,直接返回
                    resolve(true);
                }
            }).catch(async (err)=> {
                console.log(err);
                let flag = await getAuthToken();
                //判断是否 token 请求成功
                if( flag ) {
                    //更新app状态
                    Taro.$store.dispatch(changeAppOnLaunch());
                    resolve(true);
                }else{
                    //提示
                    Taro.showToast({
                        title : '获取授权信息失败' ,
                        icon : 'none' ,
                        mask : true
                    })
                }
            })
        }
    })
}
    
    // 检查令牌是否有效 true--> 有效  false--> 无效
    static checkAuth(){
        const state = Taro.$store.getState();
        //从缓存读取授权信息
        let authorize = state.authorize || Taro.getStorageSync('authorize') || {},
            expiryTime = 0,
            nowTime = ~~(Date.now() / 1000);
    
        if (authorize.exp) {
            expiryTime = authorize.exp;
        }
    
        return expiryTime - nowTime > 300;
    }

    //获取token
    static getToken(){
        const state = Taro.$store.getState();
        let authorize = state.authorize || Taro.getStorageSync('authorize');
        return authorize.token;
    }
}

//授权用户 token
async function getAuthToken(){
    const state = Taro.$store.getState();
    //login
    let res = await Taro.login();
    //获取token
    let response = await Taro.request({
            url : `${state.app.baseURL}api/xxx/xxx` ,
            data : {
                code : res.code 
            } ,
            method : 'POST'
    })
    //判断是否成功
    if( response.data.data && response.data.data.authorize ){
        //写入token
        let authorize = response.data.data.authorize;
        saveAuthToken(authorize);
        return true;
    }else{
        console.log('获取token失败');
        return false;
    }
}

//写入信息
function saveAuthToken (authorize) {
    //写入状态管理
    Taro.$store.dispatch(insertToken(authorize));
    //写入缓存
    Taro.setStorageSync('authorize',authorize)
}

上面总共定义不少方法,我都有注释,相信大家也都能看懂,其主要思想就是,如果用户首次进入小程序,通过login进行登录,并将令牌存入本地,后面通过getToken方法进行获取请求的令牌,以后用户在进入小程序的时候,就进行授权信息和登录状态检测,如果登录过期或者令牌过期则通过getAuthToken方法进行请求,获取最新的身份令牌!

上面之所以要触发一次 changeAppOnLaunch 是因为用户每次进入需要进行一次checkSeesion检测,如果用户本次session_key未过期,一般来说本次小程序生命周期内就就不需要在进行checkSession检测了,因此我们用一个状态来标识用户是否是重新进入小程序~

step3

假定我们的页面的初始化请求都写在componentDidMount生命周期内,那么现在我们要做的就是要在页面接口请求之前去判断是否已经获取到token,如果有token则进行正常的接口请求,否则就通过上面的登录获取token,不然就会导致接口未携带令牌而发生错误~

那么问题来了,我们如何阻塞componentDidMount而保证每次接口请求之前都已经拿到了token呢,这里我的做法就是对Component进行二次封装,采用的是ES7提供的装饰器,关于装饰器的使用,可以瞄一眼这篇文章~

通过装饰器,我们可以将Component进行二次封装,可以根据业务情况将授权、分享等全部进行归一化处理,这里我们创建一个pageInit.js,来实现这个功能~

//pageInit.js

import Taro from '@tarojs/taro'
import auth from '../utils/auth'
function pageInit() {
    return function Component(Component) {
        return class extends Component {

        constructor (props){
            super(props);
        }
        
        //onLoad
        componentWillMount(){
            //初始分享信息
            initShareMenu(this.state);
        }
        
        //阻塞 didMount , 鉴权
        async componentDidMount() {
            let result = await auth.appCheckAuth();
            //授权成功
            if( result ){
                //调用父组件的函数
                super.componentDidMount && super.componentDidMount();
            }else{
                //授权失败
                Taro.showToast({
                    title : '授权失败' ,
                    icon : 'none' ,
                    mask : true
                })
            }
        }
        
        //重写分享
        onShareAppMessage(){
            let shareOptions = super.onShareAppMessage();
            //如果当前页面配置分享使用配置的
            if( shareOptions ) return shareOptions;
            //默认分享
            return {
                title : '默认分享内容'
            }
        }

        //重新下拉刷新
        onPullDownRefresh(){
            if(super.onPullDownRefresh){
                super.onPullDownRefresh();
                setTimeout(() => {
                    Taro.stopPullDownRefresh();
                }, 1500)
            }
        }
    }
};
}

/**
 * 初始化分享信息
 */
function initShareMenu (state) {
    // 初始化页面分享信息
    if ( state && state.canShare ) {
        Taro.showShareMenu({
            withShareTicket: false
        })
    } else {
        Taro.hideShareMenu();
    }
}

export default pageInit;

核心思想是,通过asyncawait 阻塞,让其先执行自身的 componentDidMount 方法,拿到授权信息后,在调用父类的 componentDidMount 方法,这样就可以保证授权信息获取到再进行接口请求了~

我们要做的就是在页面内通过 @pageInit() 进行装饰即可~

这里我做了个测试,在清空全部授权信息后,在页面进行一次接口请求,可以看到先执行的是 lite 获取用户令牌,然后进行页面的接口请求,保证了执行顺序不会错乱~

小结

ok,到此为止,我们基本上已经算是完成了整套体系,接下来就是在接口请求中加上token就好了,这里我就补不贴代码了~
完成的代码可去仓库自行查看~

Taro对于我来说,也算是属于初探,这篇文章也是希望能给跟我一样萌新的小伙伴一个抛砖引玉的作用,如果有哪里写的不对的地方,还请批评斧正~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容