我的编程空间,编程开发者的网络收藏夹
学习永远不晚

详解ASP.NET Core Web Api之JWT刷新Token

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

详解ASP.NET Core Web Api之JWT刷新Token

前言

如题,本节我们进入JWT最后一节内容,JWT本质上就是从身份认证服务器获取访问令牌,继而对于用户后续可访问受保护资源,但是关键问题是:访问令牌的生命周期到底设置成多久呢?见过一些使用JWT的童鞋会将JWT过期时间设置成很长,有的几个小时,有的一天,有的甚至一个月,这么做当然存在问题,如果被恶意获得访问令牌,那么可在整个生命周期中使用访问令牌,也就是说存在冒充用户身份,此时身份认证服务器当然也就是始终信任该冒牌访问令牌,若要使得冒牌访问令牌无效,唯一的方案则是修改密钥,但是如果我们这么做了,则将使得已授予的访问令牌都将无效,所以更改密钥不是最佳方案,我们应该从源头尽量控制这个问题,而不是等到问题呈现再来想解决之道,刷新令牌闪亮登场。

RefreshToken

什么是刷新令牌呢?刷新访问令牌是用来从身份认证服务器交换获得新的访问令牌,有了刷新令牌可以在访问令牌过期后通过刷新令牌重新获取新的访问令牌而无需客户端通过凭据重新登录,如此一来,既保证了用户访问令牌过期后的良好体验,也保证了更高的系统安全性,同时,若通过刷新令牌获取新的访问令牌验证其无效可将受访者纳入黑名单限制其访问,那么访问令牌和刷新令牌的生命周期设置成多久合适呢?这取决于系统要求的安全性,一般来讲访问令牌的生命周期不会太长,比如5分钟,又比如获取微信的AccessToken的过期时间为2个小时。接下来我将用两张表来演示实现刷新令牌的整个过程,可能有更好的方案,欢迎在评论中提出,学习,学习。我们新建一个http://localhost:5000的WebApi用于身份认证,再新建一个http://localhost:5001的客户端,首先点击【模拟登录获取Toen】获取访问令牌和刷新令牌,然后点击【调用客户端获取当前时间】,如下:

接下来我们新建一张用户表(User)和用户刷新令牌表(UserRefreshToken),结构如下:


public class User
  {
    public string Id { get; set; }
    public string Email { get; set; }
    public string UserName { get; set; }
    private readonly List<UserRefreshToken> _userRefreshTokens = new List<UserRefreshToken>();
    public IEnumerable<UserRefreshToken> UserRefreshTokens => _userRefreshTokens;
    /// <summary>
    /// 验证刷新token是否存在或过期
    /// </summary>
    /// <param name="refreshToken"></param>
    /// <returns></returns>
    public bool IsValidRefreshToken(string refreshToken)
    {
      return _userRefreshTokens.Any(d => d.Token.Equals(refreshToken) && d.Active);
    }
    /// <summary>
    /// 创建刷新Token
    /// </summary>
    /// <param name="token"></param>
    /// <param name="userId"></param>
    /// <param name="minutes"></param>
    public void CreateRefreshToken(string token, string userId, double minutes = 1)
    {
      _userRefreshTokens.Add(new UserRefreshToken() { Token = token, UserId = userId, Expires = DateTime.Now.AddMinutes(minutes) });
    }
    /// <summary>
    /// 移除刷新token
    /// </summary>
    /// <param name="refreshToken"></param>
    public void RemoveRefreshToken(string refreshToken)
    {
      _userRefreshTokens.Remove(_userRefreshTokens.FirstOrDefault(t => t.Token == refreshToken));
    }

public class UserRefreshToken
  {
    public string Id { get; private set; } = Guid.NewGuid().ToString();
    public string Token { get; set; }
    public DateTime Expires { get; set; }
    public string UserId { get; set; }
    public bool Active => DateTime.Now <= Expires;
  }

如上可以看到对于刷新令牌的操作我们将其放在用户实体中,也就是使用EF Core中的Back Fields而不对外暴露。接下来我们将生成的访问令牌、刷新令牌、验证访问令牌、获取用户身份封装成对应方法如下:


/// <summary>
    /// 生成访问令牌
    /// </summary>
    /// <param name="claims"></param>
    /// <returns></returns>
    public string GenerateAccessToken(Claim[] claims)
    {
      var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey));
      var token = new JwtSecurityToken(
        issuer: "http://localhost:5000",
        audience: "http://localhost:5001",
        claims: claims,
        notBefore: DateTime.Now,
        expires: DateTime.Now.AddMinutes(1),
        signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
      );
      return new JwtSecurityTokenHandler().WriteToken(token);
    }
    /// <summary>
    /// 生成刷新Token
    /// </summary>
    /// <returns></returns>
    public string GenerateRefreshToken()
    {
      var randomNumber = new byte[32];
      using (var rng = RandomNumberGenerator.Create())
      {
        rng.GetBytes(randomNumber);
        return Convert.ToBase64String(randomNumber);
      }
    }
    /// <summary>
    /// 从Token中获取用户身份
    /// </summary>
    /// <param name="token"></param>
    /// <returns></returns>
    public ClaimsPrincipal GetPrincipalFromAccessToken(string token)
    {
      var handler = new JwtSecurityTokenHandler();
      try
      {
        return handler.ValidateToken(token, new TokenValidationParameters
        {
          ValidateAudience = false,
          ValidateIssuer = false,
          ValidateIssuerSigningKey = true,
          IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey)),
          ValidateLifetime = false
        }, out SecurityToken validatedToken);
      }
      catch (Exception)
      {
        return null;
      }
    }

当用户点击登录,访问身份认证服务器,登录成功后我们创建访问令牌和刷新令牌并返回,如下:


[HttpPost("login")]
    public async Task<IActionResult> Login()
    {
      var user = new User()
      {
        Id = "D21D099B-B49B-4604-A247-71B0518A0B1C",
        UserName = "Jeffcky",
        Email = "2752154844@qq.com"
      };
      await context.Users.AddAsync(user);
      var refreshToken = GenerateRefreshToken();
      user.CreateRefreshToken(refreshToken, user.Id);
      await context.SaveChangesAsync();
      var claims = new Claim[]
      {
        new Claim(ClaimTypes.Name, user.UserName),
        new Claim(JwtRegisteredClaimNames.Email, user.Email),
        new Claim(JwtRegisteredClaimNames.Sub, user.Id),
      };
      return Ok(new Response() { AccessToken = GenerateAccessToken(claims), RefreshToken = refreshToken });
    }

此时我们回到如上给出的图,我们点击【模拟登录获取Token】,此时发出Ajax请求,然后将返回的访问令牌和刷新令牌存储到本地localStorage中,如下:


<input type="button" id="btn" value="模拟登录获取Token" />
<input type="button" id="btn-currentTime" value="调用客户端获取当前时间" />

//模拟登陆
    $('#btn').click(function () {
      GetTokenAndRefreshToken();
    });
    //获取Token
    function GetTokenAndRefreshToken() {  
     $.post('http://localhost:5000/api/account/login').done(function (data) {
        saveAccessToken(data.accessToken);
        saveRefreshToken(data.refreshToken);
      });
    }

//从localStorage获取AccessToken
    function getAccessToken() {
      return localStorage.getItem('accessToken');
    }
    //从localStorage获取RefreshToken
    function getRefreshToken() {
      return localStorage.getItem('refreshToken');
    }
    //保存AccessToken到localStorage
    function saveAccessToken(token) {
      localStorage.setItem('accessToken', token);
    }
    //保存RefreshToken到localStorage
    function saveRefreshToken(refreshToken) {
      localStorage.setItem('refreshToken', refreshToken);
    }

此时我们再来点击【调用客户端获取当前时间】,同时将登录返回的访问令牌设置到请求头中,代码如下:


$('#btn-currentTime').click(function () {
      GetCurrentTime();
    });
    //调用客户端获取当前时间
    function GetCurrentTime() {
      $.ajax({
        type: 'get',
        contentType: 'application/json',
        url: 'http://localhost:5001/api/home',
        beforeSend: function (xhr) {
          xhr.setRequestHeader('Authorization', 'Bearer ' + getAccessToken());
        },
        success: function (data) {
          alert(data);
        },
        error: function (xhr) {
        }
      });
    }

客户端请求接口很简单,为了让大家一步步看明白,我也给出来,如下:


[Authorize]
    [HttpGet("api/[controller]")]
    public string GetCurrentTime()
    {
      return DateTime.Now.ToString("yyyy-MM-dd");
    }

好了到了这里我们已经实现模拟登录获取访问令牌,并能够调用客户端接口获取到当前时间,同时我们也只是返回了刷新令牌并存储到了本地localStorage中,并未用到。当访问令牌过期后我们需要通过访问令牌和刷新令牌去获取新的访问令牌,对吧。那么问题来了。我们怎么知道访问令牌已经过期了呢?这是其一,其二是为何要发送旧的访问令牌去获取新的访问令牌呢?直接通过刷新令牌去换取不行吗?有问题是好的,就怕没有任何思考,我们一一来解答。我们在客户端添加JWT中间件时,里面有一个事件可以捕捉到访问令牌已过期(关于客户端配置JWT中间件第一节已讲过,这里不再啰嗦),如下:


options.Events = new JwtBearerEvents
         {
           OnAuthenticationFailed = context =>
           {
             if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
             {
               context.Response.Headers.Add("act", "expired");
             }
             return Task.CompletedTask;
           }
         };

通过如上事件并捕捉访问令牌过期异常,这里我们在响应头添加了一个自定义键act,值为expired,因为一个401只能反映未授权,并不能代表访问令牌已过期。当我们在第一张图中点击【调用客户端获取当前时间】发出Ajax请求时,如果访问令牌过期,此时在Ajax请求中的error方法中捕捉到,我们在如上已给出发出Ajax请求的error方法中继续进行如下补充:


  error: function (xhr) {
          if (xhr.status === 401 && xhr.getResponseHeader('act') === 'expired') {
            // 访问令牌肯定已过期
          }
        }

到了这里我们已经解决如何捕捉到访问令牌已过期的问题,接下来我们需要做的则是获取刷新令牌,直接通过刷新令牌换取新的访问令牌也并非不可,只不过还是为了安全性考虑,我们加上旧的访问令牌。接下来我们发出Ajax请求获取刷新令牌,如下:


//获取刷新Token
    function GetRefreshToken(func) {
      var model = {
        accessToken: getAccessToken(),
        refreshToken: getRefreshToken()
      };
      $.ajax({
        type: "POST",
        contentType: "application/json; charset=utf-8",
        url: 'http://localhost:5000/api/account/refresh-token',
        dataType: "json",
        data: JSON.stringify(model),
        success: function (data) {
          if (!data.accessToken && !data.refreshToken) {
            // 跳转至登录
          } else {
            saveAccessToken(data.accessToken);
            saveRefreshToken(data.refreshToken);
            func();
          }
        }
      });
    }

发出Ajax请求获取刷新令牌的方法我们传入了一个函数,这个函数则是上一次调用接口访问令牌过期的请求,点击【调用客户端获取当前时间】按钮的Ajax请求error方法中,最终演变成如下这般:


error: function (xhr) {
          if (xhr.status === 401 && xhr.getResponseHeader('act') === 'expired') {
            
            GetRefreshToken(GetCurrentTime);
          }
        }

接下来则是通过传入旧的访问令牌和刷新令牌调用接口换取新的访问令牌,如下:


/// <summary>
    /// 刷新Token
    /// </summary>
    /// <returns></returns>
    [HttpPost("refresh-token")]
    public async Task<IActionResult> RefreshToken([FromBody] Request request)
    {
      //TODO 参数校验
      var principal = GetPrincipalFromAccessToken(request.AccessToken);
      if (principal is null)
      {
        return Ok(false);
      }
      var id = principal.Claims.First(c => c.Type == JwtRegisteredClaimNames.Sub)?.Value;
      if (string.IsNullOrEmpty(id))
      {
        return Ok(false);
      }
      var user = await context.Users.Include(d => d.UserRefreshTokens)
        .FirstOrDefaultAsync(d => d.Id == id);
      if (user is null || user.UserRefreshTokens?.Count() <= 0)
      {
        return Ok(false);
      }
      if (!user.IsValidRefreshToken(request.RefreshToken))
      {
        return Ok(false);
      }
      user.RemoveRefreshToken(request.RefreshToken);
      var refreshToken = GenerateRefreshToken();
      user.CreateRefreshToken(refreshToken, id);
      try
      {
        await context.SaveChangesAsync();
      }
      catch (Exception ex)
      {
        throw ex;
      }
      var claims = new Claim[]
      {
        new Claim(ClaimTypes.Name, user.UserName),
        new Claim(JwtRegisteredClaimNames.Email, user.Email),
        new Claim(JwtRegisteredClaimNames.Sub, user.Id),
      };
      return Ok(new Response()
      {
        AccessToken = GenerateAccessToken(claims),
        RefreshToken = refreshToken
      });
    }

如上通过传入旧的访问令牌验证并获取用户身份,然后验证刷新令牌是否已经过期,如果未过期则创建新的访问令牌,同时更新刷新令牌。最终客户端访问令牌过期的那一刻,通过刷新令牌获取新的访问令牌继续调用上一请求,如下:

到这里关于JWT实现刷新Token就已结束,自我感觉此种实现刷新令牌将其存储到数据库的方案还算可取,将刷新令牌存储到Redis也可行,看个人选择吧。上述若刷新令牌验证无效,可将访问者添加至黑名单,不过是添加一个属性罢了。别着急,本节内容结束前,还留有彩蛋。

EntityFramework Core Back Fields深入探讨

无论是看视频还是看技术博客也好,一定要动手验证,看到这里觉得上述我所演示是不是毫无问题,如果阅读本文的你直接拷贝上述代码你会发现有问题,且听我娓娓道来,让我们来复习下Back Fields。Back Fields命名是有约定dei,上述我是根据约定而命名,所以千万别一意孤行,别乱来,比如如下命名将抛出如下异常:


 private readonly List<UserRefreshToken> _refreshTokens = new List<UserRefreshToken>();
 public IEnumerable<UserRefreshToken> UserRefreshTokens => _refreshTokens;

上述我们配置刷新令牌的Back Fields,代码如下:


 private readonly List<UserRefreshToken> _userRefreshTokens = new List<UserRefreshToken>();
 public IEnumerable<UserRefreshToken> UserRefreshTokens => _userRefreshTokens;

要是我们配置成如下形式,结果又会怎样呢?


 private readonly List<UserRefreshToken> _userRefreshTokens = new List<UserRefreshToken>();
 public IEnumerable<UserRefreshToken> UserRefreshTokens => _userRefreshTokens.AsReadOnly();

此时为了解决这个问题,我们必须将其显式配置成Back Fields,如下:


protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
      modelBuilder.Entity<User>(u =>
      {
        var navigation = u.Metadata.FindNavigation(nameof(User.UserRefreshTokens));
        navigation.SetPropertyAccessMode(PropertyAccessMode.Field);
      });
    }

在我个人著作中也讲解到为了性能问题,可将字段进行ToList(),若进行了ToList(),必须显式配置成Back Fields,否则获取不到刷新令牌导航属性,如下:


private readonly List<UserRefreshToken> _userRefreshTokens = new List<UserRefreshToken>();
public IEnumerable<UserRefreshToken> UserRefreshTokens => _userRefreshTokens.ToList();

或者进行如下配置,我想应该也可取,不会存在性能问题,如下:


private readonly List<UserRefreshToken> _userRefreshTokens = new List<UserRefreshToken>();
 public IReadOnlyCollection<UserRefreshToken> UserRefreshTokens => _userRefreshTokens.AsReadOnly();

这是关于Back Fields问题之一,问题之二则是上述我们请求获取刷新令牌中,我们先在刷新令牌的Back Fields中移除掉旧的刷新令牌,而后再创建新的刷新令牌,但是会抛出如下异常:

我们看到在添加刷新令牌时,用户Id是有值的,对不对,这是为何呢?究其根本问题出在我们移除刷新令牌方法中,如下:


/// <summary>
    /// 移除刷新token
    /// </summary>
    /// <param name="refreshToken"></param>
    public void RemoveRefreshToken(string refreshToken)
    {
      _userRefreshTokens.Remove(_userRefreshTokens.FirstOrDefault(t => t.Token == refreshToken));
    }

我们将查询出来的导航属性并将其映射到_userRefreshTokens字段中,此时是被上下文所追踪,上述我们查询出存在的刷新令牌并在跟踪的刷新令牌中进行移除,没毛病,没找到原因,于是乎,我将上述方法修改成如下看看是否必须需要主键才能删除旧的刷新令牌:


/// <summary>
    /// 移除刷新token
    /// </summary>
    /// <param name="refreshToken"></param>
    public void RemoveRefreshToken(string refreshToken)
    {
      var id = _userRefreshTokens.FirstOrDefault(t => t.Token == refreshToken).Id;
      _userRefreshTokens.Remove(new UserRefreshToken() { Id = id });
    }

倒没抛出异常,创建了一个新的刷新令牌,但是旧的刷新令牌却没删除,如下:

至此未找到问题出在哪里,当前版本为2.2,难道不能通过Back Fields移除对象?这个问题待解决。

总结

本节我们重点讲解了如何实现JWT刷新令牌,并也略带讨论了EF Core中Back Fields以及尚未解决的问题,至此关于JWT已结束,下节开始正式进入Docker小白系列,感谢阅读。

到此这篇关于详解ASP.NET Core Web Api之JWT刷新Token的文章就介绍到这了,更多相关ASP.NET Core Web Api JWT刷新Token内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

您可能感兴趣的文章:ASP.NET Core使用JWT认证授权的方法ASP.NET Core学习之使用JWT认证授权详解浅谈ASP.NET Core 中jwt授权认证的流程原理ASP.Net Core3.0中使用JWT认证的实现asp.net core集成JWT的步骤记录Asp.Net Core基于JWT认证的数据接口网关实例代码


免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

详解ASP.NET Core Web Api之JWT刷新Token

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

详解ASP.NET Core Web Api之JWT刷新Token

前言 如题,本节我们进入JWT最后一节内容,JWT本质上就是从身份认证服务器获取访问令牌,继而对于用户后续可访问受保护资源,但是关键问题是:访问令牌的生命周期到底设置成多久呢?见过一些使用JWT的童鞋会将JWT过期时间设置成很长,有的几个小
2022-06-07

ASP.NET Core怎么实现自动刷新JWT Token

这篇文章主要介绍了ASP.NET Core怎么实现自动刷新JWT Token的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇ASP.NET Core怎么实现自动刷新JWT Token文章都会有所收获,下面我们一起
2023-06-30

asp.net core 中的Jwt(Json Web Token)的使用详解

session不支持分布式并且在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这篇文章主要介绍了asp.net core 中的Jwt(Json Web Token)的使用,需要的朋友可以参考下
2022-11-13

详解如何在ASP.NET Core Web API中以三种方式返回数据

在 ASP.NET Core 中有三种返回 数据 和 HTTP状态码 的方式,最简单的就是直接返回指定的类型实例,如下代码所示:[ApiController][Route("[controller]")]public class Weath
2022-06-07

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录