使用identity+jwt保护你的webapi(三)——refresh token
前言
上一篇已經介紹了identity的注冊,登錄,獲取jwt token,本篇來完成refresh token。
開始
開始之前先說明一下為什么需要refresh token。
雖然jwt token有很多優點,但是它的缺點也是非常明顯。由于jwt無狀態的特性,所以jwt一旦頒發,基本上就不可控了,在過期時間內一直有效。有些場景下我們是希望能控制token失效的,比如用戶的重要數據被修改時(密碼,角色,權限,等等),我們希望用戶重新獲取token,甚至重新登錄。
那么refresh token就可以很好的彌補jwt的缺陷。雖然refresh token也無法直接控制jwt失效,但是在refresh token機制下,我們可以把token的有效期設置的短一些,比如30分鐘,而refresh token的有效期可以很長;因為refresh token會持久化到數據庫中,它是完全可控的。
很多人糾結的jwt滑動刷新,無感刷新,在refresh token機制下,都不是問題。
生成refresh_token
改造一下上一篇的代碼,首先refresh token需要持久化到數據庫中,定義實體:
public?class?RefreshToken {[Key]public?int?Id?{?get;?set;?}[Required][StringLength(128)]public?string?JwtId?{?get;?set;?}[Required][StringLength(256)]public?string?Token?{?get;?set;?}///?<summary>///?是否使用,一個RefreshToken只能使用一次///?</summary>[Required]public?bool?Used?{?get;?set;?}///?<summary>///?是否失效。修改用戶重要信息時可將此字段更新為true,使用戶重新登錄///?</summary>[Required]public?bool?Invalidated?{?get;?set;?}[Required]public?DateTime?CreationTime?{?get;?set;?}[Required]public?DateTime?ExpiryTime?{?get;?set;?}[Required]public?int?UserId?{?get;?set;?}[Required][ForeignKey(nameof(UserId))]public?AppUser?User?{?get;?set;?} }加入到DbContext:
public?class?AppDbContext?:?IdentityDbContext<AppUser,?IdentityRole<int>,?int> {public?DbSet<RefreshToken>?RefreshTokens?{?get;?set;?}public?AppDbContext(DbContextOptions<AppDbContext>?options):?base(options){}//?省略...... }ef遷移:
dotnet ef migrations add AppDbContext_Added_RefreshTokendotnet ef database update登錄注冊返回token時,也要把RefreshToken和ExpiresIn有效時間一起返回:
public?class?TokenResult {public?bool?Success?=>?Errors?==?null?||?!Errors.Any();public?IEnumerable<string>?Errors?{?get;?set;?}public?string?AccessToken?{?get;?set;?}public?string?TokenType?{?get;?set;?}public?int?ExpiresIn?{?get;?set;?}???//?addpublic?string?RefreshToken?{?get;?set;?}??//?add }public?class?TokenResponse {[JsonPropertyName("access_token")]?public?string?AccessToken?{?get;?set;?}[JsonPropertyName("token_type")]?public?string?TokenType?{?get;?set;?}[JsonPropertyName("expires_in")]public?int?ExpiresIn?{?get;?set;?}??//?add[JsonPropertyName("refresh_token")]public?string?RefreshToken?{?get;?set;?}?//?add }修改UserService創建token方法:
private?async?Task<TokenResult>?GenerateJwtToken(AppUser?user) {var?key?=?Encoding.ASCII.GetBytes(_jwtSettings.SecurityKey);var?tokenDescriptor?=?new?SecurityTokenDescriptor{Subject?=?new?ClaimsIdentity(new[]{new?Claim(JwtRegisteredClaimNames.Jti,?Guid.NewGuid().ToString("N")),new?Claim(JwtRegisteredClaimNames.Sub,?user.Id.ToString())}),IssuedAt?=?DateTime.UtcNow,NotBefore?=?DateTime.UtcNow,Expires?=?DateTime.UtcNow.Add(_jwtSettings.ExpiresIn),SigningCredentials?=?new?SigningCredentials(new?SymmetricSecurityKey(key),SecurityAlgorithms.HmacSha256Signature)};var?jwtTokenHandler?=?new?JwtSecurityTokenHandler();var?securityToken?=?jwtTokenHandler.CreateToken(tokenDescriptor);var?token?=?jwtTokenHandler.WriteToken(securityToken);var?refreshToken?=?new?RefreshToken(){JwtId?=?securityToken.Id,UserId?=?user.Id,CreationTime?=?DateTime.UtcNow,ExpiryTime?=?DateTime.UtcNow.AddMonths(6),Token?=?GenerateRandomNumber()};await?_appDbContext.RefreshTokens.AddAsync(refreshToken);await?_appDbContext.SaveChangesAsync();return?new?TokenResult(){AccessToken?=?token,TokenType?=?"Bearer",RefreshToken?=?refreshToken.Token,ExpiresIn?=?(int)_jwtSettings.ExpiresIn.TotalSeconds,}; }private?string?GenerateRandomNumber(int?len?=?32) {var?randomNumber?=?new?byte[len];using?var?rng?=?RandomNumberGenerator.Create();rng.GetBytes(randomNumber);return?Convert.ToBase64String(randomNumber); }登錄測試,已經返回了refresh_token:
使用refresh_token獲取token
//?RefreshToken?請求參數 public?class?RefreshTokenRequest {[JsonPropertyName("access_token")]public?string?AccessToken?{?get;?set;?}[JsonPropertyName("refresh_token")]public?string?RefreshToken?{?get;?set;?} }public?interface?IUserService {//?省略......Task<TokenResult>?RefreshTokenAsync(string?token,?string?refreshToken);?//?add }RefreshTokenAsync實現:
public?async?Task<TokenResult>?RefreshTokenAsync(string?token,?string?refreshToken) {var?claimsPrincipal?=?GetClaimsPrincipalByToken(token);if?(claimsPrincipal?==?null){//?無效的token...return?new?TokenResult(){Errors?=?new[]?{?"1:?Invalid?request!"?},};}var?expiryDateUnix?=long.Parse(claimsPrincipal.Claims.Single(x?=>?x.Type?==?JwtRegisteredClaimNames.Exp).Value);var?expiryDateTimeUtc?=?UnixTimeStampToDateTime(expiryDateUnix);if?(expiryDateTimeUtc?>?DateTime.UtcNow){//?token未過期...return?new?TokenResult(){Errors?=?new[]?{?"2:?Invalid?request!"?},};}var?jti?=?claimsPrincipal.Claims.Single(x?=>?x.Type?==?JwtRegisteredClaimNames.Jti).Value;var?storedRefreshToken?=await?_appDbContext.RefreshTokens.SingleOrDefaultAsync(x?=>?x.Token?==?refreshToken);if?(storedRefreshToken?==?null){//?無效的refresh_token...return?new?TokenResult(){Errors?=?new[]?{?"3:?Invalid?request!"?},};}if?(storedRefreshToken.ExpiryTime?<?DateTime.UtcNow){//?refresh_token已過期...return?new?TokenResult(){Errors?=?new[]?{?"4:?Invalid?request!"?},};}if?(storedRefreshToken.Invalidated){//?refresh_token已失效...return?new?TokenResult(){Errors?=?new[]?{?"5:?Invalid?request!"?},};}if?(storedRefreshToken.Used){//?refresh_token已使用...return?new?TokenResult(){Errors?=?new[]?{?"6:?Invalid?request!"?},};}if?(storedRefreshToken.JwtId?!=?jti){//?refresh_token與此token不匹配...return?new?TokenResult(){Errors?=?new[]?{?"7:?Invalid?request!"?},};}storedRefreshToken.Used?=?true;//_userDbContext.RefreshTokens.Update(storedRefreshToken);await?_appDbContext.SaveChangesAsync();var?dbUser?=?await?_userManager.FindByIdAsync(storedRefreshToken.UserId.ToString());return?await?GenerateJwtToken(dbUser); }解析token,注意這里的tokenValidationParameters,這個參數和Startup中設置的tokenValidationParameters唯一的區別是ValidateLifetime = false,不驗證過期時間。
private?ClaimsPrincipal?GetClaimsPrincipalByToken(string?token) {try{var?tokenValidationParameters?=?new?TokenValidationParameters{ValidateIssuer?=?false,ValidateAudience?=?false,ValidateIssuerSigningKey?=?true,IssuerSigningKey?=?new?SymmetricSecurityKey(Encoding.ASCII.GetBytes(_jwtSettings.SecurityKey)),ClockSkew?=?TimeSpan.Zero,ValidateLifetime?=?false?//?不驗證過期時間!!!};var?jwtTokenHandler?=?new?JwtSecurityTokenHandler();var?claimsPrincipal?=jwtTokenHandler.ValidateToken(token,?tokenValidationParameters,?out?var?validatedToken);var?validatedSecurityAlgorithm?=?validatedToken?is?JwtSecurityToken?jwtSecurityToken&&?jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256,StringComparison.InvariantCultureIgnoreCase);return?validatedSecurityAlgorithm???claimsPrincipal?:?null;}catch{return?null;} }最后是UserController:
[HttpPost("RefreshToken")] public?async?Task<IActionResult>?RefreshToken(RefreshTokenRequest?request) {var?result?=?await?_userService.RefreshTokenAsync(request.AccessToken,?request.RefreshToken);if?(!result.Success){return?Unauthorized(new?FailedResponse(){Errors?=?result.Errors});}return?Ok(new?TokenResponse{AccessToken?=?result.AccessToken,TokenType?=?result.TokenType,ExpiresIn?=?result.ExpiresIn,RefreshToken?=?result.RefreshToken}); }測試token未過期時刷新token:
正常刷新token:
refresh_token使用一次后,不可以再次使用:
其他情況可以自行測試。。。
最后
總結一下,上面的代碼看似很多,其實完成的功能非常簡單;就是在用戶獲取token時,后臺生成一個與之對應的refresh token一并返回,同時將refresh token保存到數據庫中;refresh token的存在就是為了當token過期時,能免登錄刷新一次token。(refresh token只能使用一次,用戶重要數據比如密碼修改時,可以將refresh token置為失效,使用戶重新登錄)。代碼已上傳至 [blogs/asp.net core identity + jwt/code at main · xiajingren/blogs (github.com)](https://github.com/xiajingren/blogs/tree/main/asp.net core identity %2B jwt/code "blogs/asp.net core identity + jwt/code at main · xiajingren/blogs (github.com)")
參考:
ASP.NET Core 簡介 Identity | Microsoft Docs[1]
Mohamad Lawand - DEV Community[2]
參考資料
[1]
ASP.NET Core 簡介 Identity | Microsoft Docs: https://docs.microsoft.com/zh-cn/aspnet/core/security/authentication/identity?view=aspnetcore-5.0&tabs=visual-studio
[2]Mohamad Lawand - DEV Community: https://dev.to/moe23/comments
總結
以上是生活随笔為你收集整理的使用identity+jwt保护你的webapi(三)——refresh token的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Magicodes.IE 2.5.6.1
- 下一篇: 【开源框架】:解决方案级别的代码生成器