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:
- 要求用户重新登录(这不是一个好的用户体验)。
- 使用 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.cs
public 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.cs
public 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.cs
public class RefreshToken
{
public int Id { get; set; }
public string UserId { get; set; } // 连接到 ASP.Net Identity User Id
public string Token { get; set; } // Refresh Token
public string JwtId { get; set; } // 使用 JwtId 映射到对应的 token
public bool IsUsed { get; set; } // 如果已经使用过它,我们不想使用相同的 refresh token 生成新的 JWT token
public 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 token
var 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 expired
if (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 Id
var jti = tokenInVerification.Claims.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Jti).Value;
// 根据数据库中保存的 Id 验证收到的 token 的 Id
if (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();
// 生成一个新的 token
var 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
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 ︎
https://github.com/mohamadlawand087/v8-refreshtokenswithJWT ︎
Asp Net Core 5 REST API 使用 RefreshToken 刷新 JWT - Step by Step的更多相关文章
- Asp.Net Core 5 REST API - Step by Step
翻译自 Mohamad Lawand 2021年1月19日的文章 <Asp.Net Core 5 Rest API Step by Step> [1] 在本文中,我们将创建一个简单的 As ...
- Asp.Net Core 5 REST API 使用 JWT 身份验证 - Step by Step
翻译自 Mohamad Lawand 2021年1月22日的文章 <Asp Net Core 5 Rest API Authentication with JWT Step by Step> ...
- angular4和asp.net core 2 web api
angular4和asp.net core 2 web api 这是一篇学习笔记. angular 5 正式版都快出了, 不过主要是性能升级. 我认为angular 4还是很适合企业的, 就像.net ...
- ASP.NET Core 中基于 API Key 对私有 Web API 进行保护
这两天遇到一个应用场景,需要对内网调用的部分 web api 进行安全保护,只允许请求头账户包含指定 key 的客户端进行调用.在网上找到一篇英文博文 ASP.NET Core - Protect y ...
- ASP.NET Core WebApi构建API接口服务实战演练
一.ASP.NET Core WebApi课程介绍 人生苦短,我用.NET Core!提到Api接口,一般会想到以前用到的WebService和WCF服务,这三个技术都是用来创建服务接口,只不过Web ...
- 使用ASP.NET Core构建RESTful API的技术指南
译者荐语:利用周末的时间,本人拜读了长沙.NET技术社区翻译的技术标准<微软RESTFul API指南>,打算按照步骤写一个完整的教程,后来无意中看到了这篇文章,与我要写的主题有不少相似之 ...
- 温故知新,使用ASP.NET Core创建Web API,永远第一次
ASP.NET Core简介 ASP.NET Core是一个跨平台的高性能开源框架,用于生成启用云且连接Internet的新式应用. 使用ASP.NET Core,您可以: 生成Web应用和服务.物联 ...
- 使用angular4和asp.net core 2 web api做个练习项目(一)
这是一篇学习笔记. angular 5 正式版都快出了, 不过主要是性能升级. 我认为angular 4还是很适合企业的, 就像.net一样. 我用的是windows 10 安装工具: git for ...
- 使用angular4和asp.net core 2 web api做个练习项目(四)
第一部分: http://www.cnblogs.com/cgzl/p/7755801.html 第二部分: http://www.cnblogs.com/cgzl/p/7763397.html 第三 ...
随机推荐
- ES-Next classes static properties
ES-Next classes static properties https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/ ...
- 线上 S1 故障是什么, 线上 S1 故障, 运维故障分级, 运维, 故障分级, P1 级别故障, 故障, P1 , S1
线上 S1 故障是什么 线上 S1 故障, 运维故障分级, 运维, 故障分级, P1 级别故障, 故障, P1 , S1 故障复盘 https://time.geekbang.org/column/a ...
- js in depth: event loop & micro-task, macro-task & stack, queue, heap & thread, process
js in depth: event loop & micro-task, macro-task & stack, queue, heap & thread, process ...
- 苏黎世财经对话区块链专家,NGK如何利用时间价值实现自身的垂直扩张?
近日,苏黎世财经日报联合法兰西金融等多家知名媒体,专访了NGK. 苏黎世财经日报专栏记者玛科尔德表示,随着NGK DeFi的明星代币BGV登上去中心化金融的舞台,它千倍的收益率让生态投资者趋之若鹜. ...
- Baccarat如何点燃DEFI市场?
目前DeFi是成为了各大生态的"兵家必争之地",与此同时DeFi的高收益也成为吸引散户入局的一个利器.而虽然流动性挖矿板块近期的温度有所下降,但是这其中不乏还是有很多收益颇丰的De ...
- synchronized语法
synchronized( ){ } synchronized 关键字是加锁的意思,用它来修饰方法就表示给该方法加了锁,从而达到线程同步的效果;用它来修饰代码块就表示给该代码块加了锁,从而达到线程同步 ...
- HDFS中的NameNode名节点——FSimage
HDFS缓冲区 Fsimage 文件映射,Edits文件操作记录. 与ES的缓冲区不同,ES是维护数据的变更,而HDFS缓冲区是用于名结点维护文件系统元数据(目录树)的机制. 在HDFS集群中,Nam ...
- 解决tui-editor布局错误失效问题(Vue-Element-Admin)
更新:突然想起来会不会是兼容或者版本问题? 使用tui-editor遇到markdown预览样式错误,也不清楚是不是版本的问题,改了下源码之后效果正常了 打开控制台看到几个类名不对劲,te-edito ...
- Git:使用远程仓库
远程仓库可使用Github.Gitee,或自建Gitlab.Gogs服务器,这里使用Github. 配置本地用户名和邮箱 # 配置本地用户的用户名邮箱(保存在用户.gitconfig文件) $ git ...
- c++指针数组与二维数组的最大区别
下面随笔是关于指针数组说明及与二维数组的最大区别. 指针数组 数组的元素是指针型 例 利用指针数组存放矩阵 1 #include 2 using namespace std; 3 int main() ...