从壹开始先后端 [vue后台] 之二 || 完美实现 JWT 滑动受权刷新

缘起

哈喽你们周一好!不知道小伙伴们有没有学习呀,近来发现各类俱乐部搞起来了,啥时候群里小伙伴也搞一次分享会吧,好歹也是半千了(时间真快,还记得5个月前只有20多人),以前在上个公司,虽然也参与组织过几回活动,这个再说吧,毕竟都是五湖四海的小伙伴,不太好聚😂。今天要说的内容很简单,可是我的感受很实用,从文章标题就可见一斑:JWT的滑动受权,这个问题我被问了不下 n 次,从 6 个月前开始第一次写 JWT 受权,就有小伙伴陆陆续续在群里提问,说如何然这种无序化的 Token 令牌(不像 Session 那样,一直存在会话状态),达到滑动刷新,实现用户的无感知受权,我也一直在思考,大抵有如下一些思路:前端

一、token 失效后,直接跳转到登陆页; // UE体验感贼差vue

二、将 JWT 、 用户标识 ( 如:id ) 、过时时间等令牌信息存到数据库,配合用户进行操做; // 额外的操做太多,链接数据库ios

三、一样的上边的这些信息放到 Redis 里,再配合缓存,也能够高效处理;// 虽然不操做数据库,可是变相破坏Token的无序性git

四、返回前端两个token,经过 refresh_token 来刷新 access_token;// 本文要说明的,和这个相似的策略方法github

五、在后端处理,Id4中,自带了 RefreshToken,自动能够从新获取token;// 这个在下一个系列说到 Id4 的时候,会说到;redis

 

这些方法和策略我也一直和群里小伙伴讨论,可是却一直没有写文章,也一直没有真正的经过代码写出来,之前偷懒是由于只有后台 .net core 项目,后来本身又偷懒说只有博客项目,直观上很差实现,如今好了,终于在这段时间上线了后台管理系统,终于把这个问题提上了日程,那下边就开始今天的说明吧。数据库

老规矩,仍是先看效果(这篇文章比较简单,可是有一丢丢的绕,但愿看的时候,能够有十多分钟的安静时间,不要着急,本身研究出来的永远比问出来的要高效的多):axios

 

故事背景:后端

当前 Token 将于 18:05:14 失效,之前的状况是,在失效后,直接跳转到登陆页,可是如今不是了,api

在 18:05:19 的时候,执行查询,咱们从新对 Token 进行无缝刷新,而后自动重发请求并成功加载数据,是否是达到了你想要的目的?

 

老张说,这只是对JWT使用者简单处理,若是高并发,或者大数据,更安全验证,仍是建议使用ID4,若是小公司本身用,目前这个就够了。若是你有不少顾虑和疑问,请看下边的评论席,确定会有小伙伴和你有相似的心情,欢迎批评指正,最后:想要更好的受权需求,仍是用Id4,JWT只不过是练手。

那这个究竟是如何实现的呢,复杂不复杂呢?若是是你想要的,请往下看 👍,保证每一个人都能看懂,前提是你有 JWT 基础,至少用过。

 

1、个人设计思路是怎样的 

一、传统的受权流程

 

传统的受权登陆呢,很简单,也很直白,就是咱们平时使用的,由于不像 session 那样,能够一直保持着状态,当咱们的 Token 失效了之后,就只能从新获取一个新的 Token 令牌,这不只仅是它的优势也是一个缺点,

优势就是能够支持分布式,多点式的访问,session 就不能实现分布式;

缺点固然也是显而易见,当其过时了,就没法续签,或者一直保持激活状态;

咱们就只能从新获取一个,因此通常有的开发者就索性把 Token 的过时时间定的很长,好比一天,一周,甚至十天,只有用户在当前电脑上登陆一次,之后就能够随心访问了,除非本身手动点击退出登陆,说真的,这种状况我也在使用,由于咱们公司的项目有一些是内部的前台项目,好比一个Tool,一个图表系统,或者一个简单的我的数据展现,一不怕被外网看到,不会被篡改,二没有公司其余人来使用个人电脑,我就定义了一个月的失效时间,平时就彻底不用登陆了,想一想也是能够的。

 

可是,更多的是须要用户去实时登陆的,相信你们也用过一直公网的管理后台,关闭浏览器或者一段时间不操做之后,就会提示须要咱们从新登陆,因此咱们就会把 Token 的失效时间定义的很短暂,好比个人一些项目就是 30 分钟,或者一个小时,这样不只更安全,并且也能够应对那些存在变化的,好比后台管理系统,当前用户的角色变了,总不能还用以前的令牌吧,因此短时的 Token 刷新仍是颇有必要的。

这样就会出现一个问题,如何实现滑动受权,就是在流程上,Token仍是会失效,可是在用户体验 UE 上,实现无感操做,让用户在没有察觉的状况下,实现这个功能,你能够先停下来,想一想如何设计,若是想好了,请继续往下看,看是否和你的思路一致。

 

二、实现滑动的受权流程

 

这两个流程图对比起来,不一样点就在于虚线的问题,由以前的失效即跳转到登陆页,多了一个选择——在用户活跃期内,经过旧的 token 换取新的 token 继续体系内循环,这样就达到了效果(这里还有一种,就是同时发放两个 token 到前端,一个是access_token,一个是 refresh_token,我作了等价处理,其实这两种是同样的)。

这样不只能知足无缝刷新的问题,还能保持 Token 的无序性,那具体的如何在项目中使用呢,请往下继续看。

 

2、实现滑动的三步走 

从上边的流程图中,咱们能够看出来,其实要实现滑动刷新很简单,只须要咱们在 Token 失效的时候,从新获取一个 token,并从新执行一个请求便可,因此我总结了如下三个步骤:

一、定义刷新时间戳

你必定会好奇为何定义一个刷新时间,不知道你是否还记得上边我刚刚说到了,其实通常的作法是:每次登陆,向前端丢两个 token,当咱们的 access_token 失效的时候,就判断 refresh_token 是否有效,若是 refresh_token 有效,咱们就把这个 refresh_token 带到资源服务器,换取新的 access_token,这样就实现了咱们的目的。

可是咱们不想这么操做,太麻烦,还须要生成两个,因此就人为的在前端定义了一个刷新时间点,只要在这个时间点内而且 token 失效了,我就用这个失效的 token 获取新的token:

在 Login.vue 页面中定义一个刷新时间:

 var token = data.token;
 _this.$store.commit("saveToken", token);// 保存 token

 var curTime = new Date();
 var expiredate = new Date(curTime.setSeconds(curTime.getSeconds() + data.expires_in)); // 定义过时时间
 _this.$store.commit("saveTokenExpire", expiredate); // 保存过时时间
 window.localStorage.refreshtime = expiredate; // 保存刷新时间,这里的和过时时间一致

 

在浏览器中查看两个时间:

 

二、当执行操做时更新刷新时间(重要)

 A:定义方法:

我在 api.js 文件中,定义了保存刷新时间的方法 saveRefreshtime() ,这个的做用主要是记录当前用户的操做活跃期,当在这个活跃期内,就能够滑动更新,若是超过了这个时期,就跳转到登陆页:

export const saveRefreshtime = params => {

    let nowtime = new Date();
    let lastRefreshtime = window.localStorage.refreshtime ? new Date(window.localStorage.refreshtime) : new Date(-1);
    let expiretime = new Date(Date.parse(window.localStorage.TokenExpire))

    let refreshCount=1;//滑动系数
    if (lastRefreshtime >= nowtime) {
        lastRefreshtime=nowtime>expiretime ? nowtime:expiretime;
        lastRefreshtime.setMinutes(lastRefreshtime.getMinutes() + refreshCount);
        window.localStorage.refreshtime = lastRefreshtime;
    }else {
        window.localStorage.refreshtime = new Date(-1);
    }
};

上边的方法中,红色的是重要的两点:

一、滑动系数 refreshCount

这个是什么意思呢,就是你自定义的用户的中止活跃时间段,好比你想用户最大的休眠时间是20分钟,说句人话就是,用户能够最多20分钟内不进行操做,若是20分钟后,再操做,就跳转到登陆页,若是20分钟内,继续操做,那继续更新时间,休眠时间仍是以当前时间+20分钟。

 

二、最后刷新时间 lastRefreshtime

 这个就是上边说到的,当用户操做的时候,实时更新最后的刷新时间,保证用户活跃时间一直有效,这里有一个重要的就是:

lastRefreshtime=nowtime>expiretime ? nowtime:expiretime;

我为何要这么写呢,由于你考虑一下,若是 Token 的过时时间比你本身定义的刷新时间还长,举个栗子,你后台定义的 token 过时时间是30分钟,可是你的前端页面刷新时间是20分钟,当你登陆后,30分钟内没有任何操做,再31分钟的时候,从新操做,token 确定是无效了,可是很巧,你的刷新时间也是十分钟前,那就只能去登陆页了,这样达不到刷新的目的,因此我通过大量测试,不管是token过时时间,仍是页面刷新时间,只要取一个较大者就行,而后加上滑动系数,这样就能知足各类状况,不信你能够试试。 

 

B:两处调用:

那如今既然定义了这个刷新方法,在哪里调用呢,我这里想到了两个地方,固然,你也能够根据本身的须要进行自定义设计,个人是:

1、在路由钩子里刷新; //在 router.js  的 router.beforeEach 调用方法 saveRefreshtime(),保证每次进行路由切换的时候,都激活用户活跃时间。

2、在 HttpRequest 钩子刷新;//在 api.js 的 axios.interceptors.request.use 中调用 saveRefreshtime() ,由于有可能用户长时间操做同一个页面,没有进行路由切换。

我这里就处理了这两个地方,不管是用户切换路由,仍是在同一个路由的不一样按钮操做,都能保证当前用户是在操做活跃期的,进而实现滑动的效果。

 

 

三、Token无效时,无缝获取新Token,并从新请求(核心)

 如今就到了关键时刻了,定义好了刷新时间,那如何进行滑动效果呢?请先看下边代码,重点是红色的部分:

// http response 拦截器
axios.interceptors.response.use(
    response => {
        return response;
    },
    error => {
        if (error.response) {
            if (error.response.status == 401) {
                var curTime = new Date()
                var refreshtime = new Date(Date.parse(window.localStorage.refreshtime))
// 在用户操做的活跃期内
if (window.localStorage.refreshtime && (curTime <= refreshtime)) {
// 直接将整个请求 return 出去,否则的话,请求会晚于当前请求,没法达到刷新操做
return refreshToken({token: window.localStorage.Token}).then((res) => { if (res.success) { Vue.prototype.$message({ message: 'refreshToken success! loading data...', type: 'success' }); store.commit("saveToken", res.token); var curTime = new Date(); var expiredate = new Date(curTime.setSeconds(curTime.getSeconds() + res.expires_in)); store.commit("saveTokenExpire", expiredate); error.config.__isRetryRequest = true; error.config.headers.Authorization = 'Bearer ' + res.token;
// error.config 包含了当前请求的全部信息
return axios(error.config); } else { // 刷新token失败 清除token信息并跳转到登陆页面 ToLogin() } }); } else { // 返回 401,而且不知用户操做活跃期内 清除token信息并跳转到登陆页面 ToLogin() } } // 403 无权限 if (error.response.status == 403) { Vue.prototype.$message({ message: '失败!该操做无权限', type: 'error' }); return null; } } return ""; // 返回接口返回的错误信息 } );

 

其中要注意的是三点:

一、判断是不是在用户操做活跃期,若是不在,直接跳转登陆页,反之,进行 refresh 操做;

二、return refreshToken ,这里是两个return 的第一个,须要将刷新token的网络请求返回过去,否则的话,刷新token的请求成功后,当前网络请求已经结束了,没法达到刷新的目的;

三、return axios(error.config) ,这里就是从新进行一次请求,特别是 error.config ,这个就是咱们当前请求的所有信息。

效果以下:

 

 

好啦,JWT 滑动受权刷新就到这里已经完成了,是否是很简单。

 

3、实现滑动刷新的后端方法 

一、Redis,控制 Token 颁发 

除了这个前端方法觉得,还有后端处理,设计思路也很简单,我就很少说了,简单说两句:

当用户登陆的时候,生成 access_token ,咱们把 token 存在 redis 缓存中,对应匹配用户标识,状态等,当用户修改了密码,或者当前用户的权限被超级管理修改的时候,把 redis 中的当前用户的token 也更新操做,等用户再次使用的时候,先判断当前用的 token 是否有效,而后再判断是否有权限,这样也能达到效果。若是过时了,还能够把新的token 放到 Header 中返回过去,不过这样的方法,仍是须要配合前端操做,我的感受还不如上边的方法。

若是有想尝试的小伙伴,能够本身尝试下,我简单提示一下,就是在后端项目的 PermissionHandler.cs 文件中,对当前 httpContext.Request.Headers["Authorization"] 进行获取 token 判断,至于怎么操做这里就不表了。

  

4、Github && Gitee

https://github.com/anjoy8/Blog.Admin 前端

https://github.com/anjoy8/Blog.Core 后端 

-- ♥ -- ♥ -- ♥ -- ♥ -- ♥ -- ♥ --