第三十节:Asp.Net Core中JWT刷新Token解决方案

2021-02-19 16:18

阅读:641

标签:全局变量   encode   授权   home   ott   efault   bar   before   sig   

原文:第三十节:Asp.Net Core中JWT刷新Token解决方案

一. 前言

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>Indextitle>
 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>
View Code

服务器端代码1:

(PS:如果有上面提到的特殊情况,则去掉更新机制中 4.2和4.3的代码)

技术图片技术图片
  1    public class HomeController : Controller
  2     {
  3         private static List rTokenList = new List();
  4 
  5         public IConfiguration _Configuration { get; }
  6 
  7         public HomeController(IConfiguration Configuration)
  8         {
  9             this._Configuration = Configuration;
 10         }
 11 
 12         /// 
 13         /// 测试页面
 14         /// 
 15         /// 
 16         public IActionResult Index()
 17         {
 18             return View();
 19         }
 20 
 21         /// 
 22         /// 校验登录
 23         /// 
 24         /// 
 25         /// 
 26         /// 
 27         [HttpPost]
 28         public IActionResult CheckLogin(string userAccount, string userPwd)
 29         {
 30 
 31             if (userAccount == "admin" && userPwd == "123456")
 32             {
 33 
 34                 string AccessTokenKey = _Configuration["AccessTokenKey"];
 35                 string RefreshTokenKey = _Configuration["RefreshTokenKey"];
 36 
 37                 //1.先去数据库中吧userId查出来
 38                 string userId = "001";
 39 
 40                 //2. 生成accessToken
 41                 //过期时间(下面表示签名后 5分钟过期,这里设置20s为了演示)
 42                 double exp = (DateTime.UtcNow.AddSeconds(20) - new DateTime(1970, 1, 1)).TotalSeconds;
 43                 var payload = new Dictionarystring, object>
 44                     {
 45                          {"userId", userId },
 46                          {"userAccount", userAccount },
 47                          {"exp",exp }
 48                     };
 49                 var accessToken = JWTHelp.JWTJiaM(payload, AccessTokenKey);
 50 
 51                 //3.生成refreshToken
 52                 //过期时间(可以不设置,下面表示 2天过期)
 53                 var expireTime = DateTime.Now.AddDays(2);
 54                 double exp2 = (expireTime - new DateTime(1970, 1, 1)).TotalSeconds;
 55                 var payload2 = new Dictionarystring, object>
 56                     {
 57                          {"userId", userId },
 58                          {"userAccount", userAccount },
 59                          {"exp",exp2 }
 60                     };
 61                 var refreshToken = JWTHelp.JWTJiaM(payload2, RefreshTokenKey);
 62 
 63                 //4.将生成refreshToken的原始信息存到数据库/Redis中 (这里暂时存到一个全局变量中)
 64                 //先查询有没有,有则更新,没有则添加
 65                 var RefreshTokenItem = rTokenList.Where(u => u.userId == userId).FirstOrDefault();
 66                 if (RefreshTokenItem == null)
 67                 {
 68                     RefreshToken rItem = new RefreshToken()
 69                     {
 70                         id = Guid.NewGuid().ToString("N"),
 71                         userId = userId,
 72                         expire = expireTime,
 73                         Token = refreshToken
 74                     };
 75                     rTokenList.Add(rItem);
 76 
 77                 }
 78                 else
 79                 {
 80                     RefreshTokenItem.Token = refreshToken;
 81                     RefreshTokenItem.expire = expireTime;   //要和前面生成的过期时间相匹配
 82 
 83                 }
 84                 return Json(new
 85                 {
 86                     status = "ok",
 87                     msg="登录成功",
 88                     data = new
 89                     {
 90                         accessToken,
 91                         refreshToken
 92                     }
 93                 });
 94             }
 95             else
 96             {
 97                 return Json(new
 98                 {
 99                     status = "error",
100                     msg = "登录失败",
101                     data = new { }
102                 });
103             }
104 
105 
106         }
107 
108 
109 
110         /// 
111         /// 获取系统信息接口
112         /// 
113         /// 
114         [TypeFilter(typeof(JwtCheck2))]
115         public IActionResult GetMsg()
116         {
117             string msg = "windows10";
118             return Json(new { status = "ok", msg = msg });
119         }
120 
121 
122 
123         /// 
124         /// 更新访问令牌(同时也更新刷新令牌)
125         /// 
126         /// 
127         public IActionResult UpdateAccessToken(string accessToken, string refreshToken)
128         {
129 
130             string AccessTokenKey = _Configuration["AccessTokenKey"];
131             string RefreshTokenKey = _Configuration["RefreshTokenKey"];
132 
133             //1.先通过纯代码校验refreshToken的物理合法性
134             var result = JWTHelp.JWTJieM(refreshToken, _Configuration["RefreshTokenKey"]);
135             if (result== "expired"|| result == "invalid" || result == "error")
136             {
137                 return Json(new { status = "error", data = "" });
138             }
139 
140             //2.从accessToken中解析出来userId等其它数据(即使accessToken已经过期,依旧可以解析出来)
141             JwtData myJwtData = JsonConvert.DeserializeObject(this.Base64UrlDecode(accessToken.Split(.)[1]));
142 
143             //3. 拿着userId、refreshToken、当前时间去RefreshToken表中查数据
144             var rTokenItem = rTokenList.Where(u => u.userId == myJwtData.userId && u.Token == refreshToken && u.expire > DateTime.Now).FirstOrDefault();
145             if (rTokenItem==null)
146             {
147                 return Json(new { status = "error", data = "" });
148             }
149 
150             //4.重新生成 accessToken和refreshToken,并写入RefreshToken表
151             //4.1. 生成accessToken
152             //过期时间(下面表示签名后 5分钟过期,这里设置20s为了演示)
153             double exp = (DateTime.UtcNow.AddSeconds(20) - new DateTime(1970, 1, 1)).TotalSeconds;
154             var payload = new Dictionarystring, object>
155                     {
156                          {"userId", myJwtData.userId },
157                          {"userAccount", myJwtData.userAccount },
158                          {"exp",exp }
159                     };
160             var MyAccessToken = JWTHelp.JWTJiaM(payload, AccessTokenKey);
161 
162             //4.2.生成refreshToken
163             //过期时间(可以不设置,下面表示签名后 2天过期)
164             var expireTime = DateTime.Now.AddDays(2);
165             double exp2 = (expireTime - new DateTime(1970, 1, 1)).TotalSeconds;
166             var payload2 = new Dictionarystring, object>
167                     {
168                          {"userId", myJwtData.userId },
169                          {"userAccount", myJwtData.userAccount },
170                          {"exp",exp2 }
171                     };
172             var MyRefreshToken = JWTHelp.JWTJiaM(payload2, RefreshTokenKey);
173 
174             //4.3 更新refreshToken表
175             rTokenItem.Token = MyRefreshToken;
176             rTokenItem.expire = expireTime;
177 
178 
179             //5. 返回双Token
180             return Json(new
181             {
182                 status = "ok",
183                 data = new
184                 {
185                     accessToken= MyAccessToken,
186                     refreshToken= MyRefreshToken
187                 }
188             });
189 
190         }
191 
192 
193         /// 
194         /// Base64解码
195         /// 
196         /// 
197         /// 
198 
199         public string Base64UrlDecode(string base64UrlStr)
200         {
201             base64UrlStr = base64UrlStr.Replace(-, +).Replace(_, /);
202             switch (base64UrlStr.Length % 4)
203             {
204                 case 2:
205                     base64UrlStr += "==";
206                     break;
207                 case 3:
208                     base64UrlStr += "=";
209                     break;
210             }
211             var bytes = Convert.FromBase64String(base64UrlStr);
212             return Encoding.UTF8.GetString(bytes);
213         }
214      
215 
216     }
相关接口

 服务器端代码2:

技术图片技术图片
 1  /// 
 2     /// Jwt的加密和解密
 3     /// 注:加密和加密用的是用一个密钥
 4     /// 依赖程序集:【JWT】
 5     /// 
 6     public class JWTHelp
 7     {
 8 
 9         /// 
10         /// JWT加密算法
11         /// 
12         /// 负荷部分,存储使用的信息
13         /// 密钥
14         /// 存放表头额外的信息,不需要的话可以不传
15         /// 
16         public static string JWTJiaM(IDictionarystring, object> payload, string secret, IDictionarystring, object> extraHeaders = null)
17         {
18             IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
19             IJsonSerializer serializer = new JsonNetSerializer();
20             IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
21             IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
22             var token = encoder.Encode(payload, secret);
23             return token;
24         }
25 
26         /// 
27         /// JWT解密算法
28         /// 
29         /// 需要解密的token串
30         /// 密钥
31         /// 
32         public static string JWTJieM(string token, string secret)
33         {
34             try
35             {
36                 IJsonSerializer serializer = new JsonNetSerializer();
37                 IDateTimeProvider provider = new UtcDateTimeProvider();
38                 IJwtValidator validator = new JwtValidator(serializer, provider);
39                 IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
40                 IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder);
41                 
42                 var json = decoder.Decode(token, secret, true);
43                 //校验通过,返回解密后的字符串
44                 return json;
45             }
46             catch (TokenExpiredException)
47             {
48                 //表示过期
49                 return "expired";
50             }
51             catch (SignatureVerificationException)
52             {
53                 //表示验证不通过
54                 return "invalid";
55             }
56             catch (Exception)
57             {
58                 return "error";
59             }
60         }
61 
62 
63     }
JWT帮助类

 服务器端代码3:

技术图片技术图片
 1  public class RefreshToken
 2     {
 3         //主键


评论


亲,登录后才可以留言!