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.登录请求过来,将userIduserAccount存到payLoad中,设置不同的过期时间,分别生成accessTokenrefreshToken,二者的区别密钥不一样,过期时间不一样,然后把 生成refreshToken的相关信息存到对应的表中【id,userId,token,expire】,一个用户对应一条记录(也可以存到Redis中,这里为了测试,存在一个全局变量中), 每次登录的时候,添加或者更新记录,最后将双Token返回给前端,前端存到LocalStorage中。
在这里插入图片描述
img
2.前端访问GetMsg获取信息接口,表头需要携带accessToken,服务器端通过JwtCheck2过滤器进行校验,验证通过则正常访问,如果不通过返回401和不通过的原因,前端在Error中进行获取,这里区分造成401的原因。

//获取信息接口
        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);
                    }
                },
                //当安全校验未通过的时候进入这里
                error: function (xhr) {
                    if (xhr.status == 401) {
                        var errorMsg = xhr.responseText;
                        console.log(errorMsg);
                        //alert(errorMsg);
                        if (errorMsg == "expired") {
                            //表示过期,需要自动刷新
                            GetTokenAgain(GetMsg);
                        } else {
                            //表示是非法请求,给出提示,可以直接退回登录页
                            alert("非法请求");
                        }
                    }
                }
            });
        }

3.如果是表头为空、校验错误等等,则直接提示请求非法,返回登录页。
img
4.如果捕获的是expired即过期,则调用GetTokenAgain(func)方法,即重新获取accessTokenrefreshToken,这里func代表传递进来一个方法名,以便调用成功后重新调用原方法,实现无缝刷新; 向服务器端传递 双Token, 服务器端的验证逻辑如下:

(1). 先通过纯代码校验refreshToken的物理合法性,如果非法,前端直接报错,返回到登录页面。

(2). 从accessToken中解析出来userId等其它数据(即使accessToken已经过期,依旧可以解析出来)

(3). 拿着userId、refreshToken、当前时间去RefreshToken表中查数据,如果查不到,直接返回前端保存,返回到登录页面。

(4). 如果能查到,重新生成 accessTokenrefreshToken,并写入RefreshToken

(5). 向前端返回双token,前端进行覆盖存储,然后自动调用原方法,携带新的accessToken,进行访问,从而实现无缝刷新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;
                       // 表示重新获取令牌失败,可以退回登录页
                       alert("重新获取令牌失败");

                   } else {
                       window.localStorage.setItem("accessToken", data.data.accessToken);
                       window.localStorage.setItem("refreshToken", data.data.refreshToken);
                       func();
                   }
               }
           });

PS:以上方案,适用于单个页面发送单个ajax请求,如果是多个请求,有顺序的发送,比如第一个发送完,然后再发送第二个,这种场景是没问题的。
但是,特殊情况如果一个页面多个ajax并行的过来了,如果其中有一个accessToken过期了,那么它会走更新token的机制,这时候refreshTokenaccessToken都更新了(数据库中refreshToken也更新了),会导致刚才同时进来的其它ajaxrefreshToken验证不过,从而无法刷新双token
针对这种特殊情况,作为取舍,更新accessToken的方法中,不更新refreshToken, 那么refreshToken过期,本来也是要进入 登录页的,所以针对这类情况,这种取舍也无可厚非。

下面分享完整版代码:

前端代码:

@{
    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();
            });
        });

        //登录接口
        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);
                    }
                },
                //当安全校验未通过的时候进入这里
                error: function (xhr) {
                    if (xhr.status == 401) {
                        console.log(xhr.responseText);
                        alert(xhr.responseText)
                    }
                }
            });

        }

        //获取信息接口
        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);
                    }
                },
                //当安全校验未通过的时候进入这里
                error: function (xhr) {
                    if (xhr.status == 401) {
                        var errorMsg = xhr.responseText;
                        console.log(errorMsg);
                        //alert(errorMsg);
                        if (errorMsg == "expired") {
                            //表示过期,需要自动刷新
                            GetTokenAgain(GetMsg);
                        } else {
                            //表示是非法请求,给出提示,可以直接退回登录页
                            alert("非法请求");
                        }
                    }
                }
            });
        }

        //重新获取访问令牌和刷新令牌
        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;
                        // 表示重新获取令牌失败,可以退回登录页
                        alert("重新获取令牌失败");

                    } else {
                        window.localStorage.setItem("accessToken", data.data.accessToken);
                        window.localStorage.setItem("refreshToken", data.data.refreshToken);
                        func();
                    }
                }
            });
        }

    </script>
</head>
<body>
    <button id="btn1">模拟登陆逻辑</button>
    <button id="btn2">获取系统信息</button>

</body>
</html>

服务器端代码1:

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

相关接口

public class HomeController : Controller
{
    private static List<RefreshToken> rTokenList = new List<RefreshToken>();

    public IConfiguration _Configuration { get; }

    public HomeController(IConfiguration Configuration)
    {
        this._Configuration = Configuration;
    }

    /// <summary>
    /// 测试页面
    /// </summary>
    /// <returns></returns>
    public IActionResult Index()
    {
        return View();
    }

    /// <summary>
    /// 校验登录
    /// </summary>
    /// <param name="userAccount"></param>
    /// <param name="userPwd"></param>
    /// <returns></returns>
    [HttpPost]
    public IActionResult CheckLogin(string userAccount, string userPwd)
    {

        if (userAccount == "admin" && userPwd == "123456")
        {

            string AccessTokenKey = _Configuration["AccessTokenKey"];
            string RefreshTokenKey = _Configuration["RefreshTokenKey"];

            //1.先去数据库中吧userId查出来
            string userId = "001";

            //2. 生成accessToken
            //过期时间(下面表示签名后 5分钟过期,这里设置20s为了演示)
            double exp = (DateTime.UtcNow.AddSeconds(20) - new DateTime(1970, 1, 1)).TotalSeconds;
            var payload = new Dictionary<string, object>
                     {
                          {"userId", userId },
                          {"userAccount", userAccount },
                          {"exp",exp }
                     };
            var accessToken = JWTHelp.JWTJiaM(payload, AccessTokenKey);

            //3.生成refreshToken
            //过期时间(可以不设置,下面表示 2天过期)
            var expireTime = DateTime.Now.AddDays(2);
            double exp2 = (expireTime - new DateTime(1970, 1, 1)).TotalSeconds;
            var payload2 = new Dictionary<string, object>
                     {
                          {"userId", userId },
                          {"userAccount", userAccount },
                          {"exp",exp2 }
                     };
            var refreshToken = JWTHelp.JWTJiaM(payload2, RefreshTokenKey);

            //4.将生成refreshToken的原始信息存到数据库/Redis中 (这里暂时存到一个全局变量中)
            //先查询有没有,有则更新,没有则添加
            var RefreshTokenItem = rTokenList.Where(u => u.userId == userId).FirstOrDefault();
            if (RefreshTokenItem == null)
            {
                RefreshToken rItem = new RefreshToken()
                {
                    id = Guid.NewGuid().ToString("N"),
                    userId = userId,
                    expire = expireTime,
                    Token = refreshToken
                };
                rTokenList.Add(rItem);

            }
            else
            {
                RefreshTokenItem.Token = refreshToken;
                RefreshTokenItem.expire = expireTime;   //要和前面生成的过期时间相匹配

            }
            return Json(new
            {
                status = "ok",
                msg = "登录成功",
                data = new
                {
                    accessToken,
                    refreshToken
                }
            });
        }
        else
        {
            return Json(new
            {
                status = "error",
                msg = "登录失败",
                data = new { }
            });
        }


    }



    /// <summary>
    /// 获取系统信息接口
    /// </summary>
    /// <returns></returns>
    [TypeFilter(typeof(JwtCheck2))]
    public IActionResult GetMsg()
    {
        string msg = "windows10";
        return Json(new { status = "ok", msg = msg });
    }



    /// <summary>
    /// 更新访问令牌(同时也更新刷新令牌)
    /// </summary>
    /// <returns></returns>
    public IActionResult UpdateAccessToken(string accessToken, string refreshToken)
    {

        string AccessTokenKey = _Configuration["AccessTokenKey"];
        string RefreshTokenKey = _Configuration["RefreshTokenKey"];

        //1.先通过纯代码校验refreshToken的物理合法性
        var result = JWTHelp.JWTJieM(refreshToken, _Configuration["RefreshTokenKey"]);
        if (result == "expired" || result == "invalid" || result == "error")
        {
            return Json(new { status = "error", data = "" });
        }

        //2.从accessToken中解析出来userId等其它数据(即使accessToken已经过期,依旧可以解析出来)
        JwtData myJwtData = JsonConvert.DeserializeObject<JwtData>(this.Base64UrlDecode(accessToken.Split('.')[1]));

        //3. 拿着userId、refreshToken、当前时间去RefreshToken表中查数据
        var rTokenItem = rTokenList.Where(u => u.userId == myJwtData.userId && u.Token == refreshToken && u.expire > DateTime.Now).FirstOrDefault();
        if (rTokenItem == null)
        {
            return Json(new { status = "error", data = "" });
        }

        //4.重新生成 accessToken和refreshToken,并写入RefreshToken表
        //4.1. 生成accessToken
        //过期时间(下面表示签名后 5分钟过期,这里设置20s为了演示)
        double exp = (DateTime.UtcNow.AddSeconds(20) - new DateTime(1970, 1, 1)).TotalSeconds;
        var payload = new Dictionary<string, object>
                     {
                          {"userId", myJwtData.userId },
                          {"userAccount", myJwtData.userAccount },
                          {"exp",exp }
                     };
        var MyAccessToken = JWTHelp.JWTJiaM(payload, AccessTokenKey);

        //4.2.生成refreshToken
        //过期时间(可以不设置,下面表示签名后 2天过期)
        var expireTime = DateTime.Now.AddDays(2);
        double exp2 = (expireTime - new DateTime(1970, 1, 1)).TotalSeconds;
        var payload2 = new Dictionary<string, object>
                     {
                          {"userId", myJwtData.userId },
                          {"userAccount", myJwtData.userAccount },
                          {"exp",exp2 }
                     };
        var MyRefreshToken = JWTHelp.JWTJiaM(payload2, RefreshTokenKey);

        //4.3 更新refreshToken表
        rTokenItem.Token = MyRefreshToken;
        rTokenItem.expire = expireTime;


        //5. 返回双Token
        return Json(new
        {
            status = "ok",
            data = new
            {
                accessToken = MyAccessToken,
                refreshToken = MyRefreshToken
            }
        });

    }


    /// <summary>
    /// Base64解码
    /// </summary>
    /// <param name="base64UrlStr"></param>
    /// <returns></returns>

    public string Base64UrlDecode(string base64UrlStr)
    {
        base64UrlStr = base64UrlStr.Replace('-', '+').Replace('_', '/');
        switch (base64UrlStr.Length % 4)
        {
            case 2:
                base64UrlStr += "==";
                break;
            case 3:
                base64UrlStr += "=";
                break;
        }
        var bytes = Convert.FromBase64String(base64UrlStr);
        return Encoding.UTF8.GetString(bytes);
    }
}

服务器端代码2:

JWT帮助类

/// <summary>
/// Jwt的加密和解密
/// 注:加密和加密用的是用一个密钥
/// 依赖程序集:【JWT】
/// </summary>
public class JWTHelp
{
    /// <summary>
    /// JWT加密算法
    /// </summary>
    /// <param name="payload">负荷部分,存储使用的信息</param>
    /// <param name="secret">密钥</param>
    /// <param name="extraHeaders">存放表头额外的信息,不需要的话可以不传</param>
    /// <returns></returns>
    public static string JWTJiaM(IDictionary<string, object> payload, string secret, IDictionary<string, object> extraHeaders = null)
    {
        IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
        IJsonSerializer serializer = new JsonNetSerializer();
        IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
        IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
        var token = encoder.Encode(payload, secret);
        return token;
    }

    /// <summary>
    /// JWT解密算法
    /// </summary>
    /// <param name="token">需要解密的token串</param>
    /// <param name="secret">密钥</param>
    /// <returns></returns>
    public static string JWTJieM(string token, string secret)
    {
        try
        {
            IJsonSerializer serializer = new JsonNetSerializer();
            IDateTimeProvider provider = new UtcDateTimeProvider();
            IJwtValidator validator = new JwtValidator(serializer, provider);
            IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
            IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder);

            var json = decoder.Decode(token, secret, true);
            //校验通过,返回解密后的字符串
            return json;
        }
        catch (TokenExpiredException)
        {
            //表示过期
            return "expired";
        }
        catch (SignatureVerificationException)
        {
            //表示验证不通过
            return "invalid";
        }
        catch (Exception)
        {
            return "error";
        }
    }
}

服务器端代码3:

实体类

public class RefreshToken
{
    //主键
    public string id { get; set; }
    //用户编号
    public string userId { get; set; }
    //refreshToken
    public string Token { get; set; }
    //过期时间
    public DateTime expire { get; set; }
}

public class JwtData
{
    public DateTime expire { get; set; }  //代表过期时间

    public string userId { get; set; }

    public string userAccount { get; set; }
}

过滤器代码:

/// <summary>
/// Bearer认证,返回ajax中的error
/// 校验访问令牌的合法性
/// </summary>
public class JwtCheck2 : ActionFilterAttribute
{

    private IConfiguration _configuration;
    public JwtCheck2(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    /// <summary>
    /// action执行前执行
    /// </summary>
    /// <param name="context"></param>
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        //1.判断是否需要校验
        var isSkip = context.ActionDescriptor.EndpointMetadata.Any(x => x.GetType() == typeof(SkipAttribute));
        if (isSkip == false)
        {
            //2. 判断是什么请求(ajax or 非ajax)
            var actionContext = context.HttpContext;
            if (IsAjaxRequest(actionContext.Request))
            {
                //表示是ajax
                var token = context.HttpContext.Request.Headers["Authorization"].ToString();    //ajax请求传过来
                string pattern = "^Bearer (.*?)$";
                if (!Regex.IsMatch(token, pattern))
                {
                    context.Result = new ContentResult { StatusCode = 401, Content = "token格式不对!格式为: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不能为空" };
                    return;
                }
                //校验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
                {
                    //表示校验通过,用于向控制器中传值
                    context.RouteData.Values.Add("auth", result);
                }

            }
            else
            {
                //表示是非ajax请求,则auth拼接在参数中传过来
                context.Result = new RedirectResult("/Home/NoPerIndex?reason=null");
                return;
            }
        }

    }


    /// <summary>
    /// 判断该请求是否是ajax请求
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    private bool IsAjaxRequest(HttpRequest request)
    {
        string header = request.Headers["X-Requested-With"];
        return "XMLHttpRequest".Equals(header);
    }
}

测试

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

img
img

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,针对您的问题,我可以给您一些关于js-cookie设置token过期时间以及ASP.NET Core Web API之JWT刷新Token的实用技巧。 首先,关于js-cookie设置token过期时间,可以通过以下代码实现: ```javascript // 设置cookie过期时间为1天 Cookies.set('token', 'your_token_value', { expires: 1 }); ``` 其,`expires`属性表示过期时间,单位为天。 其次,关于ASP.NET Core Web API之JWT刷新Token,可以采用以下方法: 在JWT的payload加入`exp`属性,表示token的过期时间。当token过期后,客户端需要向服务器发送请求,获取新的token。服务器对于过期的token,可以返回一个特定的状态码,比如401 Unauthorized。客户端接收到该状态码后,可以重新向服务器发送请求,获取新的token。 同时,在服务器端需要实现一个刷新token的接口,接口的功能是根据旧的token生成新的token。具体实现可以参考以下代码: ```csharp [HttpPost("refresh-token")] public IActionResult RefreshToken([FromBody] string token) { // 验证旧的token是否有效 var principal = _jwtService.GetPrincipalFromExpiredToken(token); if (principal == null) return BadRequest("Invalid token"); // 生成新的token var newToken = _jwtService.GenerateToken(principal.Claims); return Ok(newToken); } ``` 其,`_jwtService`表示JWT的服务类,`GetPrincipalFromExpiredToken`方法用于从过期的token获取`ClaimsPrincipal`对象,`GenerateToken`方法用于生成新的token。 希望以上内容对您有所帮助。如有其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值