Asp Net Core 5 REST API 使用 RefreshToken 刷新 JWT - Step by Step(三)

翻译自 Mohamad Lawand 2021年1月25日的文章 《Refresh JWT with Refresh Tokens in Asp Net Core 5 Rest API Step by Step》 [1]

在本文中,我将向您演示如何在 Asp.Net Core REST API 中将 Refresh Token 添加到 JWT 身份验证。

我们将覆盖的一些主题包含:Refresh Token、一些新的 Endpoints 功能和 JWT(JSON Web Token)。

你也可以在 YouTube 上观看完整的视频[2],还可以下载源代码[3]

这是 REST API 开发系列的第三部分,前面还有:

  • Part 1:Asp.Net Core 5 REST API - Step by Step

  • Part 2:Asp.Net Core 5 REST API 使用 JWT 身份验证 - Step by Step

我将基于在上一篇文章中创建的 Todo REST API 应用程序进行当前的讲述。您可以通过阅读上一篇文章并与我一起构建应用程序,或者可以从 github 下载上一篇中的源代码。

在开始实现 Refresh Token 功能之前,让我们先来了解一下 Refresh Token 的运行逻辑是怎样的。

本质上,JWT token 有一个过期时间,时间越短越安全。在 JWT token 过期后,有两种方法可以获取新的 token:

  1. 要求用户重新登录(这不是一个好的用户体验)。

  2. 使用 Refresh Token 自动重新验证用户并生成新的 JWT token。

那么,Refresh Token 是什么呢?一个 Refresh Token 可以是任何东西,从字符串到 Guid 到任意组合,只要它是唯一的

为什么短暂生命周期的 JWT token 很重要,这是因为如果有人窃取了我们的 JWT token 并开始请求我们的服务器,那么该 token 在过期(变得不可用)之前只会持续一小段时间。获取新 token 的唯一方法是使用 Refresh Token 或登录。

另一个重点是,如果用户更改了密码,则根据之前的用户凭据生成的所有 token 会怎样呢。我们并不想使所有会话都失效,我们只需请求刷新 Token,那么将生成一个基于新凭证的新 JWT token。

另外,实现自动刷新 token 的一个好办法是,在客户端发出每个请求之前,都需要检查 token 的过期时间,如果已过期,我们就请求一个新的 token,否则就使用现有的 token 执行请求。

因此,我们将在应用程序中添加一个 Refresh Token,而不仅仅是在每次授权时都只生成一个 JWT token。

那就让我们开始吧,首先我们将更新 Startup 类,通过将 TokenValidationParameters 添加到依赖注入容器,使它在整个应用程序中可用。

var key = Encoding.ASCII.GetBytes(Configuration["JwtConfig:Secret"]);var tokenValidationParams = new TokenValidationParameters
{ValidateIssuerSigningKey = true,IssuerSigningKey = new SymmetricSecurityKey(key),ValidateIssuer = false,ValidateAudience = false,ValidateLifetime = true,RequireExpirationTime = false,ClockSkew = TimeSpan.Zero
};services.AddSingleton(tokenValidationParams);services.AddAuthentication(options =>
{options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(jwt =>
{jwt.SaveToken = true;jwt.TokenValidationParameters = tokenValidationParams;
});

更新完 Startup 类以后,我们需要更新 AuthManagementController 中的 GenerateJwtToken 函数,将 TokenDescriptor 的 Expires 值从之前的值更新为 30 秒(比较合理的值为 5~10 分钟,这里设置为 30 秒只是作演示用),我们需要把它指定的更短一些。

译者注:
实际使用时,可以在 appsettings.json 中为 JwtConfig 添加一个代表 token 过期时间的 ExpiryTimeFrame 配置项,对应的在 JwtConfig 类中添加一个 ExpiryTimeFrame 属性,然后赋值给 TokenDescriptor 的 Expires,这样 token 的过期时间就变得可配置了。

private string GenerateJwtToken(IdentityUser user)
{var jwtTokenHandler = new JwtSecurityTokenHandler();var key = Encoding.ASCII.GetBytes(_jwtConfig.Secret);var tokenDescriptor = new SecurityTokenDescriptor{Subject = new ClaimsIdentity(new[]{new Claim("Id", user.Id),new Claim(JwtRegisteredClaimNames.Email, user.Email),new Claim(JwtRegisteredClaimNames.Sub, user.Email),new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())}),Expires = DateTime.UtcNow.AddSeconds(30), // 比较合理的值为 5~10 分钟,这里设置 30 秒只是作演示用SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)};var token = jwtTokenHandler.CreateToken(tokenDescriptor);var jwtToken = jwtTokenHandler.WriteToken(token);return jwtToken;
}

接下来的步骤是更新 Configuration 文件夹中的 AuthResult,我们需要为 Refresh Token 添加一个新属性:

// Configuration\AuthResult.cspublic class AuthResult
{public string Token { get; set; }public string RefreshToken { get; set; }public bool Success { get; set; }public List<string> Errors { get; set; }
}

我们将在 Models/DTOs/Requests 中添加一个名为 TokenRequest 的新类,该类负责接收稍后我们将创建的新 Endpoint 的请求参数,用于管理刷新 Token。

// Models\DTOs\Requests\TokenRequest.cspublic class TokenRequest
{/// <summary>/// 原 Token/// </summary>[Required]public string Token { get; set; }/// <summary>/// Refresh Token/// </summary>[Required]public string RefreshToken { get; set; }
}

下一步是在我们的 Models 文件夹中创建一个名为 RefreshToken 的新模型。

// Models\RefreshToken.cspublic class RefreshToken
{public int Id { get; set; }public string UserId { get; set; } // 连接到 ASP.Net Identity User Idpublic string Token { get; set; }  // Refresh Tokenpublic string JwtId { get; set; } // 使用 JwtId 映射到对应的 tokenpublic bool IsUsed { get; set; } // 如果已经使用过它,我们不想使用相同的 refresh token 生成新的 JWT tokenpublic bool IsRevorked { get; set; } // 是否出于安全原因已将其撤销public DateTime AddedDate { get; set; }public DateTime ExpiryDate { get; set; } // refresh token 的生命周期很长,可以持续数月[ForeignKey(nameof(UserId))]public IdentityUser User {get;set;}
}

添加 RefreshToken 模型后,我们需要更新 ApiDbContext 类:

public virtual DbSet<RefreshToken> RefreshTokens { get; set; }

现在让我们为 ApiDbContext 创建数据库迁移,以便可以反映数据库中的更改:

dotnet ef migrations add "Added refresh tokens table"
dotnet ef database update

下一步是在 AuthManagementController 中创建一个新的名为 RefreshToken 的 Endpoind。需要做的第一件事是注入 TokenValidationParameters

private readonly UserManager<IdentityUser> _userManager;
private readonly JwtConfig _jwtConfig;
private readonly TokenValidationParameters _tokenValidationParams;
private readonly ApiDbContext _apiDbContext;public AuthManagementController(UserManager<IdentityUser> userManager,IOptionsMonitor<JwtConfig> optionsMonitor,TokenValidationParameters tokenValidationParams,ApiDbContext apiDbContext)
{_userManager = userManager;_jwtConfig = optionsMonitor.CurrentValue;_tokenValidationParams = tokenValidationParams;_apiDbContext = apiDbContext;
}

注入所需的参数后,我们需要更新 GenerateJwtToken 函数以包含 Refresh Token:

private async Task<AuthResult> GenerateJwtToken(IdentityUser user)
{var jwtTokenHandler = new JwtSecurityTokenHandler();var key = Encoding.ASCII.GetBytes(_jwtConfig.Secret);var tokenDescriptor = new SecurityTokenDescriptor{Subject = new ClaimsIdentity(new[]{new Claim("Id", user.Id),new Claim(JwtRegisteredClaimNames.Email, user.Email),new Claim(JwtRegisteredClaimNames.Sub, user.Email),new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())}),Expires = DateTime.UtcNow.AddSeconds(30), // 比较合理的值为 5~10 分钟,这里设置 30 秒只是作演示用SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)};var token = jwtTokenHandler.CreateToken(tokenDescriptor);var jwtToken = jwtTokenHandler.WriteToken(token);var refreshToken = new RefreshToken(){JwtId = token.Id,IsUsed = false,IsRevorked = false,UserId = user.Id,AddedDate = DateTime.UtcNow,ExpiryDate = DateTime.UtcNow.AddMonths(6),Token = RandomString(25) + Guid.NewGuid()};await _apiDbContext.RefreshTokens.AddAsync(refreshToken);await _apiDbContext.SaveChangesAsync();return new AuthResult(){Token = jwtToken,Success = true,RefreshToken = refreshToken.Token};
}private string RandomString(int length)
{var random = new Random();var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";return new string(Enumerable.Repeat(chars, length).Select(x => x[random.Next(x.Length)]).ToArray());
}

现在,让我们更新两个现有 Action 的返回值,因为我们已经更改了 GenerateJwtToken 的返回类型

Login Action:

return Ok(await GenerateJwtToken(existingUser));

Register Action:

return Ok(await GenerateJwtToken(newUser));

然后,我们可以开始构建 RefreshToken Action:

[HttpPost]
[Route("RefreshToken")]
public async Task<IActionResult> RefreshToken([FromBody] TokenRequest tokenRequest)
{if (ModelState.IsValid){var result = await VerifyAndGenerateToken(tokenRequest);if (result == null){return BadRequest(new RegistrationResponse(){Errors = new List<string>() {"Invalid tokens"},Success = false});}return Ok(result);}return BadRequest(new RegistrationResponse(){Errors = new List<string>() {"Invalid payload"},Success = false});
}private async Task<AuthResult> VerifyAndGenerateToken(TokenRequest tokenRequest)
{var jwtTokenHandler = new JwtSecurityTokenHandler();try{// Validation 1 - Validation JWT token format// 此验证功能将确保 Token 满足验证参数,并且它是一个真正的 token 而不仅仅是随机字符串var tokenInVerification = jwtTokenHandler.ValidateToken(tokenRequest.Token, _tokenValidationParams, out var validatedToken);// Validation 2 - Validate encryption alg// 检查 token 是否有有效的安全算法if (validatedToken is JwtSecurityToken jwtSecurityToken){var result = jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase);if (result == false){return null;}}// Validation 3 - validate expiry date// 验证原 token 的过期时间,得到 unix 时间戳var utcExpiryDate = long.Parse(tokenInVerification.Claims.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Exp).Value);var expiryDate = UnixTimeStampToDateTime(utcExpiryDate);if (expiryDate > DateTime.UtcNow){return new AuthResult(){Success = false,Errors = new List<string>() {"Token has not yet expired"}};}// validation 4 - validate existence of the token// 验证 refresh token 是否存在,是否是保存在数据库的 refresh tokenvar storedRefreshToken = await _apiDbContext.RefreshTokens.FirstOrDefaultAsync(x => x.Token == tokenRequest.RefreshToken);if (storedRefreshToken == null){return new AuthResult(){Success = false,Errors = new List<string>() {"Refresh Token does not exist"}};}// Validation 5 - 检查存储的 RefreshToken 是否已过期// Check the date of the saved refresh token if it has expiredif (DateTime.UtcNow > storedRefreshToken.ExpiryDate){return new AuthResult(){Errors = new List<string>() { "Refresh Token has expired, user needs to re-login" },Success = false};}// Validation 6 - validate if used// 验证 refresh token 是否已使用if (storedRefreshToken.IsUsed){return new AuthResult(){Success = false,Errors = new List<string>() {"Refresh Token has been used"}};}// Validation 7 - validate if revoked// 检查 refresh token 是否被撤销if (storedRefreshToken.IsRevorked){return new AuthResult(){Success = false,Errors = new List<string>() {"Refresh Token has been revoked"}};}// Validation 8 - validate the id// 这里获得原 JWT token Idvar jti = tokenInVerification.Claims.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Jti).Value;// 根据数据库中保存的 Id 验证收到的 token 的 Idif (storedRefreshToken.JwtId != jti){return new AuthResult(){Success = false,Errors = new List<string>() {"The token doesn't mateched the saved token"}};}// update current token // 将该 refresh token 设置为已使用storedRefreshToken.IsUsed = true;_apiDbContext.RefreshTokens.Update(storedRefreshToken);await _apiDbContext.SaveChangesAsync();// 生成一个新的 tokenvar dbUser = await _userManager.FindByIdAsync(storedRefreshToken.UserId);return await GenerateJwtToken(dbUser);}catch (Exception ex){if (ex.Message.Contains("Lifetime validation failed. The token is expired.")){return new AuthResult(){Success = false,Errors = new List<string>() {"Token has expired please re-login"}};}else{return new AuthResult(){Success = false,Errors = new List<string>() {"Something went wrong."}};}}
}private DateTime UnixTimeStampToDateTime(long unixTimeStamp)
{var dateTimeVal = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);dateTimeVal = dateTimeVal.AddSeconds(unixTimeStamp).ToLocalTime();return dateTimeVal;
}

最后,我们需要确保一切可以正常构建和运行。

dotnet build
dotnet run

当我们确定一切 OK 后,我们可以使用 Postman 测试应用程序,测试场景如下所示:

  • 登录,生成带有刷新令牌的 JWT 令牌 ⇒ 成功

  • 不等待令牌过期而直接尝试刷新令牌 ⇒ 失败

  • 等待 JWT 令牌过期然后请求刷新令牌 ⇒ 成功

  • 重新使用相同的刷新令牌 ⇒ 失败

感谢您花时间阅读本文。

本文是 API 开发系列的第三部分,你可以通过下面链接阅读前两部分:

  • Part 1:Asp.Net Core 5 REST API - Step by Step

  • Part 2:Asp.Net Core 5 REST API 使用 JWT 身份验证 - Step by Step


相关链接:

  1. https://dev.to/moe23/refresh-jwt-with-refresh-tokens-in-asp-net-core-5-rest-api-step-by-step-3en5 Refresh JWT with Refresh Tokens in Asp Net Core 5 Rest API Step by Step ↩︎

  2. https://youtu.be/T_Hla1WzaZQ ↩︎

  3. https://github.com/mohamadlawand087/v8-refreshtokenswithJWT ↩︎

作者 :Mohamad Lawand
译者 :技术译民
出品 :技术译站(https://ITTranslator.cn/)

END

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/303254.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

国外的程序猿可以工作到退休而国内的为什么这么短命(思维认知)

首先我想说明的是国外的程序猿也存在加班&#xff0c;他们也要赶项目&#xff0c;所以加班不算什么原因。真正的原因是我们国内的很大一部分程序猿只是工具的使用者&#xff0c;不会去思考工具的产生和原理&#xff0c;用惯了一个高效的工具就被这个工具套牢成为奴隶&#xff0…

使用 docker 部署 mdnice

使用 docker 部署 mdniceIntro最近 mdnice 的在线版使用开始需要登录才能访问&#xff0c;一方面我觉得我的文章会被保存的他们的服务器上&#xff0c;使用他们的平台就能轻松拿到很多文章的数据&#xff0c;他们甚至是可以直接拿到 markdown 原始内容去别的平台分享转发&#…

细数近年来机器学习研究的几大怪现状

人工智能领域的发展离不开学者们的贡献&#xff0c;然而随着研究的进步&#xff0c;越来越多的论文出现了「标题党」、「占坑」、「注水」等现象&#xff0c;暴增的顶会论文接收数量似乎并没有带来更多技术突破。最近&#xff0c;来自卡耐基梅隆大学的助理教授 Zachary C. Lipt…

50K求聘.NET,我们是认真的!

今年的节奏特别快&#xff0c;不知道你有没有同感&#xff1f;春节刚过&#xff0c;跳槽季仓促开始&#xff0c;打了个疫苗&#xff0c;过了个清明&#xff0c;一转头4月过半&#xff0c;金三银四就快尾声了&#xff0c;真是时光飞逝。当然&#xff0c;手速快的已经offer拿到手…

linux编译redis打包,linux下下载redis,并且编译

1&#xff1a;下载、解压、编译$ wget http://download.redis.io/releases/redis-3.0.6.tar.gz$ tar xzf redis-3.0.6.tar.gz$ cd redis-3.0.6$ make2&#xff1a;结果显示很多error&#xff0c;127&#xff0c;1&#xff0c;2之类的&#xff0c;然后我参考了这个网站https://w…

github star破13k,Dapr 能否引领云原生中间件的未来?

Dapr&#xff08;Distributed Application Runtime &#xff0c;分布式应用运行时&#xff09;是微软新推出的&#xff0c;一种可移植的、serverless 的、事件驱动的运行时&#xff0c;它使开发人员可以轻松构建弹性&#xff0c;无状态和有状态微服务&#xff0c;这些服务运行在…

现在的便签本都这么社会了!?重复写万次还能云端保存

不知模友们有没有感觉到每天脑子要记的事情越来越多不说事情杂七杂八重点是精力有限刚说完的事转眼就忘了想过在电脑贴满便利贴想过本子记录好每天要做什么But ......每天忙的天昏地黑啥都不记得了又懒的随身带着本子太重不好携带真想能有一个神器让这一切不再纠结一款神奇又黑…

C#类类型“.NET技术”

类类型是由字段数据&#xff08;成员变量&#xff09;和操作字段数据的成员&#xff08;属性、方法、构造函数、事件等&#xff09;所构成的自定义类型。其中字段数据表示类实例&#xff08;对象&#xff09;的状态。 在C#中&#xff0c;类使用class关键字定义&#xff0c;例如…

linux ext4 格式化工具,ext4格式化软件 mkfs.ext4 快速格式化

如何在 Windows 下访问 ext4 格式的硬盘你好&#xff0c;介绍两个能在 Windows 下读取ext4分区的软件。第一个是 Ext2Read。它能查看 ext2/3/4 分区并从中拷贝文件和目录&#xff0c;支持 LVM2 和 EXT4 extent &#xff0c;以及递归拷贝整个目录。第二个是本站之前介绍过的 Ext…

信心满满的去面算法工程师,竟然凉了...

我是小A&#xff0c;一个没能当成算法工程师的菜鸡Java工程师&#xff0c;内心却等着上AI这趟车。去年正是人工智能火热的时候&#xff0c;看着各种高薪招聘&#xff0c;我沉寂很久的内心也火热起来了。但是想归想&#xff0c;我内心还是有很多纠结的。自己已经很多年没有碰过高…

使用 Redis Stream 实现消息队列

使用 Redis Stream 实现消息队列IntroRedis 5.0 中增加了 Stream 的支持&#xff0c;利用 Stream 我们可以实现可靠的消息队列&#xff0c;并且支持一个消息被多个消费者所消费&#xff0c;可以很好的实现消息队列Simple Usage首先我们来看一个简单版本的 Stream 使用&#xff…

用linux命令通常做什么,如何知道你在 Linux 里最常使用的几个命令?

不知道大家自接触 Linux 以来&#xff0c;都使用过哪些命令&#xff0c;其中最常用的命令是什么&#xff1f;我最常用的命令之一是 sudo &#xff0c;因为我每天都在使用它在 Linux 上安装、更新、删除软件包以及其它各种需要超级用户权限的操作。那么你知道你自己最经常使用的…

资料分享 | python机器学习教程分享来袭

小天从大学开始&#xff0c;便开启资料收集功能。近几年以AlphaGo为契机&#xff0c;人工智能进入新的发展阶段&#xff0c;再加上日常的深入研究&#xff0c;小天收集整理了丰富的机器学习资料&#xff0c;内容涵盖“机器学习视频”&#xff0c;“机器学习教程”等。截止到今天…

Autofac框架初识与应用

一、前言这上一篇中&#xff0c;主要讲述了什么是IoC容器&#xff0c;以及了解到它是DI构造函注入的框架&#xff0c;它管理着依赖项的生命周期以及映射关系&#xff0c;同时也介绍实践了在ASP.Net Core中,默认提供的内置IoC容器&#xff0c;以及它的实例注册方式和相应的生命周…

聊一聊数据导出那些事

前言 数据导出&#xff0c;这可以说是一个随处可见的需求&#xff0c;大部分管理平台&#xff0c;报表系统都会有这个需求。对于这个需求&#xff0c;不少系统会做限制&#xff0c;只能从系统导出几千或几万的数据&#xff0c;再多的话就要提申请&#xff0c;经过层层审批&…

如何黑掉一台根本不联网的电脑

一直以来&#xff0c;拿到一台电脑上的密钥&#xff0c;方法无非有以下三种&#xff1a;1、直接拿到这台电脑&#xff0c;然后输入木马病毒进行盗取。&#xff08;此种略微LowB的方法风险在于&#xff1a;如果被电脑主人“捉奸在床”&#xff0c;愤而报警&#xff0c;则需要黑客…

通过Dapr实现一个简单的基于.net的微服务电商系统(二)——通讯框架讲解

首先感谢张队geffzhang公众号转发了上一篇文章&#xff0c;希望广大.neter多多推广dapr&#xff0c;让云原生更快更好的在.net这片土地上落地生根。 书接上回通过Dapr实现一个简单的基于.net的微服务电商系统&#xff0c;今天来分享一下这套电商demo的通讯部分到底是如何工作的…

windows下整合tomcat和nginx

tomcat自带的apache服务器对于并发请求的处理能力比较差&#xff0c;并且耗费资源很大&#xff0c;而nginx这方便却很强悍&#xff0c;以下是在windows下整合tomcat和nginx的过程。 1.准备工作 下载tomcat&#xff08;http://tomcat.apache.org/download-70.cgi&#xff09;,下…

从飞机上看下雨是这样子,太震撼了!

不同的角度&#xff0c;不一样的世界&#xff01;来源&#xff1a;环球顶尖摄影版权归原作者所有&#xff0c;转载仅供学习使用&#xff0c;不用于任何商业用途&#xff0c;如有侵权请留言联系删除&#xff0c;感谢合作。数据与算法之美用数据解决不可能长按扫码关注

linux基础 linhaifeng,Linux基础之命令练习Day2(示例代码)

作业一&#xff1a;1) 新建用户natasha&#xff0c;uid为1000&#xff0c;gid为555&#xff0c;备注信息为“master”2) 修改natasha用户的家目录为/Natasha3) 查看用户信息配置文件的最后一行4) 为natasha用户设置密码“123”5) 查看用户密码配置文件的最后一行6) 将natasha用…