一. 前言
1.关于JWT的Token过期问题,到底设置多久过期?
(1).有的人设置过期时间很长,比如一个月,甚至更长,等到过期了退回登录页面,重新登录重新获取token,期间登录的时候也是重新获取token,然后过期时间又重置为了1个月。这样一旦token被人截取,就可能被人长期使用,如果你想禁止,只能修改token颁发的密钥,这样就会导致所有token都失效,显然不太可取。
(2).有的人设置比较短,比如10分钟,在使用过程中,一旦过期也是退回登录页面,这样就可能使用过程中经常退回登录页面,体验很不好。
2. 这里介绍一种比较主流的解决方案---双Token机制
(1).访问令牌:accessToken,访问接口是需要携带的,也就是我们之前一直使用的那个,过期时间一般设置比较短,根据实际项目分析,比如:10分钟
(2).刷新令牌:refreshToken,当accessToken过期后,用于获取新的accessToken的时候使用,过期时间一般设置的比较长,比如:7天
3.获取新的accessToken的时候, 为什么还需要传入旧accessToken,只传入refreshToken不行么?
仔细看下面的解决思路,只传入refreshToken也可以,但是传入双Token安全性更高一些。
二. 解决方案
1. 登录请求过来,将userId和userAccount存到payLoad中,设置不同的过期时间,分别生成accessToken和refreshToken,二者的区别密钥不一样,过期时间不一样,然后把 生成refreshToken的相关信息存到对应的表中【id,userId,token,expire】,一个用户对应一条记录(也可以存到Redis中,这里为了测试,存在一个全局变量中), 每次登录的时候,添加或者更新记录,最后将双Token返回给前端,前端存到LocalStorage中。
2. 前端访问GetMsg获取信息接口,表头需要携带accessToken,服务器端通过JwtCheck2过滤器进行校验,验证通过则正常访问,如果不通过返回401和不通过的原因,前端在Error中进行获取,这里区分造成401的原因。
1 //获取信息接口 2 function GetMsg() { 3 var accessToken = window.localStorage.getItem("accessToken"); 4 $.ajax({ 5 url: "/Home/GetMsg", 6 type: "Post", 7 data: {}, 8 datatype: "json", 9 beforeSend: function (xhr) { 10 xhr.setRequestHeader("Authorization", "Bearer " + accessToken); 11 }, 12 success: function (data) { 13 if (data.status == "ok") { 14 alert(data.msg); 15 } else { 16 alert(data.msg); 17 } 18 }, 19 //当安全校验未通过的时候进入这里 20 error: function (xhr) { 21 if (xhr.status == 401) { 22 var errorMsg = xhr.responseText; 23 console.log(errorMsg); 24 //alert(errorMsg); 25 if (errorMsg == "expired") { 26 //表示过期,需要自动刷新 27 GetTokenAgain(GetMsg); 28 } else { 29 //表示是非法请求,给出提示,可以直接退回登录页 30 alert("非法请求"); 31 } 32 } 33 } 34 }); 35 }
3. 如果是表头为空、校验错误等等,则直接提示请求非法,返回登录页。
4. 如果捕获的是expired即过期,则调用GetTokenAgain(func)方法,即重新获取accessToken和refreshToken,这里func代表传递进来一个方法名,以便调用成功后重新调用原方法,实现无缝刷新; 向服务器端传递 双Token, 服务器端的验证逻辑如下:
(1). 先通过纯代码校验refreshToken的物理合法性,如果非法,前端直接报错,返回到登录页面。
(2). 从accessToken中解析出来userId等其它数据(即使accessToken已经过期,依旧可以解析出来)
(3). 拿着userId、refreshToken、当前时间去RefreshToken表中查数据,如果查不到,直接返回前端报错,返回到登录页面。
(4). 如果能查到,重新生成 accessToken和refreshToken,并写入RefreshToken表
(5). 向前端返回双token,前端进行覆盖存储,然后自动调用原方法,携带新的accessToken,进行访问,从而实现无缝刷新token的问题。
1 //重新获取访问令牌和刷新令牌 2 function GetTokenAgain(func) { 3 var model = { 4 accessToken: window.localStorage.getItem("accessToken"), 5 refreshToken: window.localStorage.getItem("refreshToken") 6 }; 7 $.ajax({ 8 url: '/Home/UpdateAccessToken', 9 type: "POST", 10 dataType: "json", 11 data: model, 12 success: function (data) { 13 if (data.status == "error") { 14 debugger; 15 // 表示重新获取令牌失败,可以退回登录页 16 alert("重新获取令牌失败"); 17 18 } else { 19 window.localStorage.setItem("accessToken", data.data.accessToken); 20 window.localStorage.setItem("refreshToken", data.data.refreshToken); 21 func(); 22 } 23 } 24 });
PS:以上方案,适用于单个页面发送单个ajax请求,如果是多个请求,有顺序的发送,比如第一个发送完,然后再发送第二个,这种场景是没问题的。
但是,特殊情况如果一个页面多个ajax并行的过来了,如果其中有一个accessToken过期了,那么它会走更新token的机制,这时候refreshToken和accessToken都更新了(数据库中refreshToken也更新了),会导致刚才同时进来的其它ajax的refreshToken验证不过,从而无法刷新双token。
针对这种特殊情况,作为取舍,更新accessToken的方法中,不更新refreshToken, 那么refreshToken过期,本来也是要进入 登录页的,所以针对这类情况,这种取舍也无可厚非。
下面分享完整版代码:
前端代码:
1 @{ 2 Layout = null; 3 } 4 5 <!DOCTYPE html> 6 7 <html> 8 <head> 9 <meta name="viewport" content="width=device-width" /> 10 <title>Index</title> 11 <script src="~/lib/jquery/dist/jquery.js"></script> 12 <script> 13 $(function () { 14 $('#btn1').click(function () { 15 Login(); 16 }); 17 $('#btn2').click(function () { 18 GetMsg(); 19 }); 20 }); 21 22 //登录接口 23 function Login() { 24 $.ajax({ 25 url: "/Home/CheckLogin", 26 type: "Post", 27 data: { userAccount: "admin", userPwd: "123456" }, 28 datatype: "json", 29 success: function (data) { 30 if (data.status == "ok") { 31 alert(data.msg); 32 console.log(data.data.accessToken); 33 console.log(data.data.refreshToken); 34 window.localStorage.setItem("accessToken", data.data.accessToken); 35 window.localStorage.setItem("refreshToken", data.data.refreshToken); 36 37 } else { 38 alert(data.msg); 39 } 40 }, 41 //当安全校验未通过的时候进入这里 42 error: function (xhr) { 43 if (xhr.status == 401) { 44 console.log(xhr.responseText); 45 alert(xhr.responseText) 46 } 47 } 48 }); 49 50 } 51 52 //获取信息接口 53 function GetMsg() { 54 var accessToken = window.localStorage.getItem("accessToken"); 55 $.ajax({ 56 url: "/Home/GetMsg", 57 type: "Post", 58 data: {}, 59 datatype: "json", 60 beforeSend: function (xhr) { 61 xhr.setRequestHeader("Authorization", "Bearer " + accessToken); 62 }, 63 success: function (data) { 64 if (data.status == "ok") { 65 alert(data.msg); 66 } else { 67 alert(data.msg); 68 } 69 }, 70 //当安全校验未通过的时候进入这里 71 error: function (xhr) { 72 if (xhr.status == 401) { 73 var errorMsg = xhr.responseText; 74 console.log(errorMsg); 75 //alert(errorMsg); 76 if (errorMsg == "expired") { 77 //表示过期,需要自动刷新 78 GetTokenAgain(GetMsg); 79 } else { 80 //表示是非法请求,给出提示,可以直接退回登录页 81 alert("非法请求"); 82 } 83 } 84 } 85 }); 86 } 87 88 //重新获取访问令牌和刷新令牌 89 function GetTokenAgain(func) { 90 var model = { 91 accessToken: window.localStorage.getItem("accessToken"), 92 refreshToken: window.localStorage.getItem("refreshToken") 93 }; 94 $.ajax({ 95 url: '/Home/UpdateAccessToken', 96 type: "POST", 97 dataType: "json", 98 data: model, 99 success: function (data) { 100 if (data.status == "error") { 101 debugger; 102 // 表示重新获取令牌失败,可以退回登录页 103 alert("重新获取令牌失败"); 104 105 } else { 106 window.localStorage.setItem("accessToken", data.data.accessToken); 107 window.localStorage.setItem("refreshToken", data.data.refreshToken); 108 func(); 109 } 110 } 111 }); 112 } 113 114 </script> 115 </head> 116 <body> 117 <button id="btn1">模拟登陆逻辑</button> 118 <button id="btn2">获取系统信息</button> 119 120 </body> 121 </html>