• 第三十节: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>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>
    View Code

    服务器端代码1:

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

      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         /// 测试页面
     14         /// </summary>
     15         /// <returns></returns>
     16         public IActionResult Index()
     17         {
     18             return View();
     19         }
     20 
     21         /// <summary>
     22         /// 校验登录
     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.先去数据库中吧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 Dictionary<string, 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 Dictionary<string, 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         /// <summary>
    111         /// 获取系统信息接口
    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         /// 更新访问令牌(同时也更新刷新令牌)
    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.先通过纯代码校验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<JwtData>(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 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.生成refreshToken
    163             //过期时间(可以不设置,下面表示签名后 2天过期)
    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 更新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         /// <summary>
    194         /// Base64解码
    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     }
    相关接口

     服务器端代码2:

     1  /// <summary>
     2     /// Jwt的加密和解密
     3     /// 注:加密和加密用的是用一个密钥
     4     /// 依赖程序集:【JWT】
     5     /// </summary>
     6     public class JWTHelp
     7     {
     8 
     9         /// <summary>
    10         /// JWT加密算法
    11         /// </summary>
    12         /// <param name="payload">负荷部分,存储使用的信息</param>
    13         /// <param name="secret">密钥</param>
    14         /// <param name="extraHeaders">存放表头额外的信息,不需要的话可以不传</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解密算法
    28         /// </summary>
    29         /// <param name="token">需要解密的token串</param>
    30         /// <param name="secret">密钥</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                 //校验通过,返回解密后的字符串
    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         //主键
     4         public string id { get; set; }
     5         //用户编号
     6         public string userId { get; set; }
     7         //refreshToken
     8         public string Token { get; set; }
     9         //过期时间
    10         public DateTime expire { get; set; }
    11     }
    12 }
    13 
    14    public class JwtData
    15     {
    16         public DateTime expire { get; set; }  //代表过期时间
    17 
    18         public string userId { get; set; }  
    19 
    20         public string userAccount { get; set; }
    21     }
    实体类

    过滤器代码:

     1  /// <summary>
     2     /// Bearer认证,返回ajax中的error
     3     /// 校验访问令牌的合法性
     4     /// </summary>
     5     public class JwtCheck2 : ActionFilterAttribute
     6     {
     7 
     8         private IConfiguration _configuration;
     9         public JwtCheck2(IConfiguration configuration)
    10         {
    11             _configuration = configuration;
    12         }
    13 
    14         /// <summary>
    15         /// action执行前执行
    16         /// </summary>
    17         /// <param name="context"></param>
    18         public override void OnActionExecuting(ActionExecutingContext context)
    19         {
    20             //1.判断是否需要校验
    21             var isSkip = context.ActionDescriptor.EndpointMetadata.Any(x => x.GetType() == typeof(SkipAttribute));
    22             if (isSkip == false)
    23             {
    24                 //2. 判断是什么请求(ajax or 非ajax)
    25                 var actionContext = context.HttpContext;
    26                 if (IsAjaxRequest(actionContext.Request))
    27                 {
    28                     //表示是ajax
    29                     var token = context.HttpContext.Request.Headers["Authorization"].ToString();    //ajax请求传过来
    30                     string pattern = "^Bearer (.*?)$";
    31                     if (!Regex.IsMatch(token, pattern))
    32                     {
    33                         context.Result = new ContentResult { StatusCode = 401, Content = "token格式不对!格式为:Bearer {token}" };
    34                         return;
    35                     }
    36                     token = Regex.Match(token, pattern).Groups[1]?.ToString();
    37                     if (token == "null" || string.IsNullOrEmpty(token))
    38                     {
    39                         context.Result = new ContentResult { StatusCode = 401, Content = "token不能为空" };
    40                         return;
    41                     }
    42                     //校验auth的正确性
    43                     var result = JWTHelp.JWTJieM(token, _configuration["AccessTokenKey"]);
    44                     if (result == "expired")
    45                     {
    46                         context.Result = new ContentResult { StatusCode = 401, Content = "expired" };
    47                         return;
    48                     }
    49                     else if (result == "invalid")
    50                     {
    51                         context.Result = new ContentResult { StatusCode = 401, Content = "invalid" };
    52                         return;
    53                     }
    54                     else if (result == "error")
    55                     {
    56                         context.Result = new ContentResult { StatusCode = 401, Content = "error" };
    57                         return;
    58                     }
    59                     else
    60                     {
    61                         //表示校验通过,用于向控制器中传值
    62                         context.RouteData.Values.Add("auth", result);
    63                     }
    64 
    65                 }
    66                 else
    67                 {
    68                     //表示是非ajax请求,则auth拼接在参数中传过来
    69                     context.Result = new RedirectResult("/Home/NoPerIndex?reason=null");
    70                     return;
    71                 }
    72             }
    73 
    74         }
    75 
    76 
    77         /// <summary>
    78         /// 判断该请求是否是ajax请求
    79         /// </summary>
    80         /// <param name="request"></param>
    81         /// <returns></returns>
    82         private bool IsAjaxRequest(HttpRequest request)
    83         {
    84             string header = request.Headers["X-Requested-With"];
    85             return "XMLHttpRequest".Equals(header);
    86         }
    87     }
    View Code

    三. 测试

       将accessToken的过期时间设置为20s,点击登录授权后,等待20s,然后点击获取信息按钮,依旧能获取信息,无缝衔接,进行了双token的更新。

     

     

    !

    • 作       者 : Yaopengfei(姚鹏飞)
    • 博客地址 : http://www.cnblogs.com/yaopengfei/
    • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
    • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
     
  • 相关阅读:
    LCA+线段树/树状数组 POJ2763 Housewife Wind
    图论 洛谷P2052 道路修建
    动态规划 洛谷P2365 任务安排
    GCD问题 洛谷P1372 又是毕业季I & P1414 又是毕业季II
    动态规划 洛谷P1140 相似基因
    动态规划 洛谷P1868 饥饿的奶牛
    动态规划 P1280 尼克的任务
    倍增LCA BZOJ1776 cowpol奶牛政坛
    P1416 攻击火星
    搜索 洛谷 P1434滑雪
  • 原文地址:https://www.cnblogs.com/yaopengfei/p/12449213.html
Copyright © 2020-2023  润新知