1, Foreword
1. Regarding the expiration of JWT Token, how long is it set to expire?
(1) Some people set the expiration time to be very long, such as one month or even longer. When it expires, they return to the login page, log in again and get the token again. During the login, they also get the token again, and then the expiration time is reset to one month. In this way, once a token is intercepted, it may be used for a long time. If you want to prohibit it, you can only modify the key issued by the token, which will lead to the invalidation of all tokens. Obviously, it is not advisable.
(2) Some people set it for a short time, such as 10 minutes. In the process of use, once it expires, they also return to the login page. In this way, they may often return to the login page during use, and the experience is very bad.
2. Here is a mainstream solution - Double Token mechanism
(1) . access token: accessToken. The access interface needs to be carried, that is, the one we have been using before. The expiration time is generally set to be short. According to the analysis of the actual project, for example, 10 minutes
(2) . refresh token: refreshToken. When the accessToken expires, it is used to obtain a new accessToken. The expiration time is generally set to be long, such as 7 days
3. When obtaining a new accessToken, why do you need to pass in the old accessToken instead of just the refreshToken?
Take a closer look at the solution below. It is OK to pass in only refreshToken, but the security of passing in double Token is higher.
2, Solution
1. After the login request, save the userId and userAccount in payLoad, set different expiration times, and generate accessToken and refreshToken respectively. The difference keys and expiration times of the two are different, and then save the relevant information of the generated refreshToken in the corresponding table [id, userId, token, expire], A user corresponds to a record (it can also be saved in Redis. Here, for testing, there is a global variable), Each time you log in, add or update records, and finally return the double token to the front end, which is stored in LocalStorage.
2. The front end accesses the GetMsg interface to obtain information. The header needs to carry an accessToken. The server side verifies it through the JwtCheck2 filter. If it passes the verification, it will access normally. If it does not pass, it returns 401 and the reason for failure, and the front end obtains it in Error. Here, the reason for 401 is distinguished.
//Get information interface function GetMsg() { var accessToken = window.localStorage.getItem("accessToken"); $.ajax({ url: "/Home/GetMsg", type: "Post", data: {}, datatype: "json", beforeSend: function (xhr) { xhr.setRequestHeader("Authorization", "Bearer " + accessToken); }, success: function (data) { if (data.status == "ok") { alert(data.msg); } else { alert(data.msg); } }, //Enter here when the security check fails error: function (xhr) { if (xhr.status == 401) { var errorMsg = xhr.responseText; console.log(errorMsg); //alert(errorMsg); if (errorMsg == "expired") { //Indicates that it has expired and needs to be refreshed automatically GetTokenAgain(GetMsg); } else { //Indicates that it is an illegal request. If you give a prompt, you can directly return to the login page alert("Illegal request"); } } } }); }
3. If the header is empty, verification error, etc., you will be prompted that the request is illegal and return to the login page.
4. If the captured is expired, that is, expired, call GetTokenAgain(func) method, that is, re obtain accessToken and refreshToken. Here func represents passing in a method name, so that the original method can be called again after the call is successful to achieve seamless refresh; Pass the double Token to the server. The verification logic of the server is as follows:
(1) . first verify the physical legitimacy of refreshToken through pure code. If it is illegal, the front end will directly report an error and return to the login page.
(2) . parse userId and other data from the accessToken (even if the accessToken has expired, it can still be parsed)
(3) Take userId, refreshtoken and current time to look up the data in the refreshtoken table. If it cannot be found, directly return to the front end with an error and return to the login page.
(4) . if it can be found, regenerate the accessToken and refreshtoken and write them into the refreshtoken table
(5) . return the double token to the front end, the front end will overwrite and store it, and then automatically call the original method to carry the new accessToken for access, so as to realize the problem of seamlessly refreshing the token.
//Re acquire access token and refresh token function GetTokenAgain(func) { var model = { accessToken: window.localStorage.getItem("accessToken"), refreshToken: window.localStorage.getItem("refreshToken") }; $.ajax({ url: '/Home/UpdateAccessToken', type: "POST", dataType: "json", data: model, success: function (data) { if (data.status == "error") { debugger; // Indicates that the token re acquisition failed and can be returned to the login page alert("Failed to retrieve token"); } else { window.localStorage.setItem("accessToken", data.data.accessToken); window.localStorage.setItem("refreshToken", data.data.refreshToken); func(); } } });
PS: the above scheme is applicable to sending a single ajax request from a single page. If there are multiple requests, they are sent in order. For example, the first one is sent, and then the second one is sent. This scenario is no problem.
However, in a special case, if multiple ajax on a page come in parallel, if one of the accesstokens has expired, it will follow the mechanism of updating the token. At this time, the refreshToken and accessToken are updated (the refreshToken in the database is also updated), which will lead to the failure of the refreshToken verification of other ajax that came in at the same time, so that the double tokens cannot be refreshed.
For this special case, as a trade-off, the refreshToken is not updated in the method of updating the accessToken, so the refreshToken expires and would have to enter the login page. Therefore, this trade-off is understandable for such cases.
Share the full code below:
Front end code:
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Index</title> <script src="~/lib/jquery/dist/jquery.js"></script> <script> $(function () { $('#btn1').click(function () { Login(); }); $('#btn2').click(function () { GetMsg(); }); }); //Login interface function Login() { $.ajax({ url: "/Home/CheckLogin", type: "Post", data: { userAccount: "admin", userPwd: "123456" }, datatype: "json", success: function (data) { if (data.status == "ok") { alert(data.msg); console.log(data.data.accessToken); console.log(data.data.refreshToken); window.localStorage.setItem("accessToken", data.data.accessToken); window.localStorage.setItem("refreshToken", data.data.refreshToken); } else { alert(data.msg); } }, //Enter here when the security check fails error: function (xhr) { if (xhr.status == 401) { console.log(xhr.responseText); alert(xhr.responseText) } } }); } //Get information interface function GetMsg() { var accessToken = window.localStorage.getItem("accessToken"); $.ajax({ url: "/Home/GetMsg", type: "Post", data: {}, datatype: "json", beforeSend: function (xhr) { xhr.setRequestHeader("Authorization", "Bearer " + accessToken); }, success: function (data) { if (data.status == "ok") { alert(data.msg); } else { alert(data.msg); } }, //Enter here when the security check fails error: function (xhr) { if (xhr.status == 401) { var errorMsg = xhr.responseText; console.log(errorMsg); //alert(errorMsg); if (errorMsg == "expired") { //Indicates that it has expired and needs to be refreshed automatically GetTokenAgain(GetMsg); } else { //Indicates that it is an illegal request. If you give a prompt, you can directly return to the login page alert("Illegal request"); } } } }); } //Re acquire access token and refresh token function GetTokenAgain(func) { var model = { accessToken: window.localStorage.getItem("accessToken"), refreshToken: window.localStorage.getItem("refreshToken") }; $.ajax({ url: '/Home/UpdateAccessToken', type: "POST", dataType: "json", data: model, success: function (data) { if (data.status == "error") { debugger; // Indicates that the token re acquisition failed and can be returned to the login page alert("Failed to retrieve token"); } else { window.localStorage.setItem("accessToken", data.data.accessToken); window.localStorage.setItem("refreshToken", data.data.refreshToken); func(); } } }); } </script> </head> <body> <button id="btn1">Simulated Login logic</button> <button id="btn2">Get system information</button> </body> </html>
Server side code 1:
(PS: if there are special cases mentioned above, remove the codes of 4.2 and 4.3 in the update mechanism)
1 public class HomeController : Controller 2 { 3 private static List<RefreshToken> rTokenList = new List<RefreshToken>(); 4 5 public IConfiguration _Configuration { get; } 6 7 public HomeController(IConfiguration Configuration) 8 { 9 this._Configuration = Configuration; 10 } 11 12 /// <summary> 13 ///Test page 14 /// </summary> 15 /// <returns></returns> 16 public IActionResult Index() 17 { 18 return View(); 19 } 20 21 /// <summary> 22 ///Verify login 23 /// </summary> 24 /// <param name="userAccount"></param> 25 /// <param name="userPwd"></param> 26 /// <returns></returns> 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. Check the userId in the database first 38 string userId = "001"; 39 40 //2. Generate accessToken 41 //Expiration time (the following indicates the expiration time of 5 minutes after signature. 20s is set here for demonstration) 42 double exp = (DateTime.UtcNow.AddSeconds(20) - new DateTime(1970, 1, 1)).TotalSeconds; 43 var payload = new Dictionary<string, object> 44 { 45 {"userId", userId }, 46 {"userAccount", userAccount }, 47 {"exp",exp } 48 }; 49 var accessToken = JWTHelp.JWTJiaM(payload, AccessTokenKey); 50 51 //3. Generate refreshToken 52 //Expiration time (it can not be set, and the following indicates 2-day expiration) 53 var expireTime = DateTime.Now.AddDays(2); 54 double exp2 = (expireTime - new DateTime(1970, 1, 1)).TotalSeconds; 55 var payload2 = new Dictionary<string, object> 56 { 57 {"userId", userId }, 58 {"userAccount", userAccount }, 59 {"exp",exp2 } 60 }; 61 var refreshToken = JWTHelp.JWTJiaM(payload2, RefreshTokenKey); 62 63 //4. Store the original information of the generated refreshToken in the database / Redis (temporarily stored in a global variable here) 64 //First query whether there is one, update if there is one, and add if there is no one 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; //To match the expiration time generated earlier 82 83 } 84 return Json(new 85 { 86 status = "ok", 87 msg="Login succeeded", 88 data = new 89 { 90 accessToken, 91 refreshToken 92 } 93 }); 94 } 95 else 96 { 97 return Json(new 98 { 99 status = "error", 100 msg = "Login failed", 101 data = new { } 102 }); 103 } 104 105 106 } 107 108 109 110 /// <summary> 111 ///Get system information interface 112 /// </summary> 113 /// <returns></returns> 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 /// <summary> 124 ///Update access token (also update refresh token) 125 /// </summary> 126 /// <returns></returns> 127 public IActionResult UpdateAccessToken(string accessToken, string refreshToken) 128 { 129 130 string AccessTokenKey = _Configuration["AccessTokenKey"]; 131 string RefreshTokenKey = _Configuration["RefreshTokenKey"]; 132 133 //1. First verify the physical legitimacy of refreshToken through pure code 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. Parse userId and other data from the accessToken (even if the accessToken has expired, it can still be parsed) 141 JwtData myJwtData = JsonConvert.DeserializeObject<JwtData>(this.Base64UrlDecode(accessToken.Split('.')[1])); 142 143 //3. Take userId, refreshtoken and current time to look up data in refreshtoken table 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. Regenerate the accessToken and refreshtoken and write them into the refreshtoken table 151 //4.1. Generate accessToken 152 //Expiration time (the following indicates the expiration time of 5 minutes after signature. 20s is set here for demonstration) 153 double exp = (DateTime.UtcNow.AddSeconds(20) - new DateTime(1970, 1, 1)).TotalSeconds; 154 var payload = new Dictionary<string, 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. Generate refreshToken 163 //Expiration time (can not be set. The following indicates that it expires 2 days after signing) 164 var expireTime = DateTime.Now.AddDays(2); 165 double exp2 = (expireTime - new DateTime(1970, 1, 1)).TotalSeconds; 166 var payload2 = new Dictionary<string, 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 update refreshToken table 175 rTokenItem.Token = MyRefreshToken; 176 rTokenItem.expire = expireTime; 177 178 179 //5. Return double 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 /// <summary> 194 ///Base64 decoding 195 /// </summary> 196 /// <param name="base64UrlStr"></param> 197 /// <returns></returns> 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 } Related interface
Server side code 2:
1 /// <summary> 2 ///Encryption and decryption of Jwt 3 ///Note: encryption and encryption use one key 4 ///Dependent assembly: [JWT] 5 /// </summary> 6 public class JWTHelp 7 { 8 9 /// <summary> 10 ///JWT encryption algorithm 11 /// </summary> 12 ///< param name = "payload" > load part, which stores the information used < / param > 13 ///< param name = "secret" > key < / param > 14 ///< param name = "extraheaders" > stores additional header information. If it is not needed, it can not be transferred to < / param > 15 /// <returns></returns> 16 public static string JWTJiaM(IDictionary<string, object> payload, string secret, IDictionary<string, 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 /// <summary> 27 ///JWT decryption algorithm 28 /// </summary> 29 ///< param name = "token" > token string to be decrypted < / param > 30 ///< param name = "secret" > key < / param > 31 /// <returns></returns> 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 //If the verification passes, the decrypted string is returned 44 return json; 45 } 46 catch (TokenExpiredException) 47 { 48 //Indicates expiration 49 return "expired"; 50 } 51 catch (SignatureVerificationException) 52 { 53 //Indicates that the verification fails 54 return "invalid"; 55 } 56 catch (Exception) 57 { 58 return "error"; 59 } 60 } 61 62 63 } JWT Help class
Server side code 3:
1 public class RefreshToken 2 { 3 //Primary key 4 public string id { get; set; } 5 //User number 6 public string userId { get; set; } 7 //refreshToken 8 public string Token { get; set; } 9 //Expiration time 10 public DateTime expire { get; set; } 11 } 12 } 13 14 public class JwtData 15 { 16 public DateTime expire { get; set; } //Represents the expiration time 17 18 public string userId { get; set; } 19 20 public string userAccount { get; set; } 21 } Entity class
Filter code:
/// <summary> ///Bearer authentication, return error in ajax ///Verify the legitimacy of the access token /// </summary> public class JwtCheck2 : ActionFilterAttribute { private IConfiguration _configuration; public JwtCheck2(IConfiguration configuration) { _configuration = configuration; } /// <summary> ///Execute before action execution /// </summary> /// <param name="context"></param> public override void OnActionExecuting(ActionExecutingContext context) { //1. Judge whether calibration is required var isSkip = context.ActionDescriptor.EndpointMetadata.Any(x => x.GetType() == typeof(SkipAttribute)); if (isSkip == false) { //2. Determine what the request is (ajax or non ajax) var actionContext = context.HttpContext; if (IsAjaxRequest(actionContext.Request)) { //Indicates ajax var token = context.HttpContext.Request.Headers["Authorization"].ToString(); //ajax request coming string pattern = "^Bearer (.*?)$"; if (!Regex.IsMatch(token, pattern)) { context.Result = new ContentResult { StatusCode = 401, Content = "token Wrong format!Format is:Bearer {token}" }; return; } token = Regex.Match(token, pattern).Groups[1]?.ToString(); if (token == "null" || string.IsNullOrEmpty(token)) { context.Result = new ContentResult { StatusCode = 401, Content = "token Cannot be empty" }; return; } //Verify the correctness of auth var result = JWTHelp.JWTJieM(token, _configuration["AccessTokenKey"]); if (result == "expired") { context.Result = new ContentResult { StatusCode = 401, Content = "expired" }; return; } else if (result == "invalid") { context.Result = new ContentResult { StatusCode = 401, Content = "invalid" }; return; } else if (result == "error") { context.Result = new ContentResult { StatusCode = 401, Content = "error" }; return; } else { //Indicates that the verification has passed, which is used to transfer values to the controller context.RouteData.Values.Add("auth", result); } } else { //Indicates that it is a non ajax request, then auth splicing is passed in the parameter context.Result = new RedirectResult("/Home/NoPerIndex?reason=null"); return; } } } /// <summary> ///Determine whether the request is an ajax request /// </summary> /// <param name="request"></param> /// <returns></returns> private bool IsAjaxRequest(HttpRequest request) { string header = request.Headers["X-Requested-With"]; return "XMLHttpRequest".Equals(header); } }
3, Testing
Set the expiration time of the accessToken to 20s, click login authorization, wait for 20s, and then click the get information button to still get information, seamlessly connect, and update the double tokens.