Jwt Token 的刷新机制设计
Jwt Token 的刷新機制設計
Intro
前面的文章我們介紹了如何實現一個簡單的 Jwt Server,可以實現一個簡單 Jwt 服務,但是使用 Jwt token 會有一個缺點就是 token 一旦頒發就不能夠進行作廢,所以通常 jwt token 的有效期一般會比較短,但是太短了又會比較影響用戶的用戶體驗,所以就有了 refresh token 的參與,一般來說 refresh token 會比實際用的 access token 有效期會長一些,當 access token 失效了,就使用 refresh token 重新獲取一個 access token,再使用新的 access_token 來訪問服務。
Sample
我們的示例在前面的基礎上增加了 refresh_token,使用示例如下:
注冊服務的時候啟用 refresh_token 就可以了
services.AddJwtTokenService(options?=> {options.SecretKey?=?Guid.NewGuid().ToString();options.Issuer?=?"https://id.weihanli.xyz";options.Audience?=?"SparkTodo";//?EnableRefreshToken,?disabled?by?defaultoptions.EnableRefreshToken?=?true; });啟用了 refresh token 之后,在生成 token 的時候就會返回一個帶著 refresh token 的 token 對象(TokenEntityWithRefreshToken) 否則就是返回只有 acess token 的對象 (TokenEntity)
public?class?TokenEntity {public?string?AccessToken?{?get;?set;?}public?int?ExpiresIn?{?get;?set;?} }public?class?TokenEntityWithRefreshToken?:?TokenEntity {public?string?RefreshToken?{?get;?set;?} }然后我們就可以使用 refresh token 來獲取新的 access token 了,使用方式如下:
[HttpGet("RefreshToken")] public?async?Task<IActionResult>?RefreshToken(string?refreshToken,?[FromServices]?ITokenService?tokenService) {return?await?tokenService.RefreshToken(refreshToken).ContinueWith(r?=>r.Result.WrapResult().GetRestResult()); }GetToken 接口和上次的示例相比稍微有一些改動,主要是體現了有沒有 refresh token 的差異,ValidateToken 和之前一致
[HttpGet("getToken")] public?async?Task<IActionResult>?GetToken([Required]?string?userName,?[FromServices]?ITokenService?tokenService) {var?token?=?await?tokenService.GenerateToken(new?Claim("name",?userName));if?(token?is?TokenEntityWithRefreshToken?tokenEntityWithRefreshToken){return?tokenEntityWithRefreshToken.WrapResult().GetRestResult();}return?token.WrapResult().GetRestResult(); }[HttpGet("validateToken")] public?async?Task<IActionResult>?ValidateToken(string?token,?[FromServices]?ITokenService?tokenService) {return?await?tokenService.ValidateToken(token).ContinueWith(r?=>r.Result.WrapResult().GetRestResult()); }驗證步驟如下:
獲取 token
refresh?token
驗證 access token
使用 refresh token 驗證 token
使用 refresh token 獲取新的 access token
new access token
驗證新的 access token
Implement
從上面 token 解析出來的內容大概可以看的出來實現的思路,我的實現思路是仍然使用 Jwt 這套機制來生成和驗證 refresh token,只是 refresh token 的 audience 和 access token 不同,另外 refresh token 的有效期一般會更長一些,這樣我們就不能把 refresh token 直接當作 access token 來使用,因為 token 驗證會失敗,而之所以利用 Jwt 的機制來實現也是希望能夠簡化 refresh token,利用 jwt 的無狀態,不需要使得無狀態的應用變得有狀態,有看過一些別的實現是直接使用存儲將 refresh token 保存起來,這樣 refresh token 就變成有狀態的了,還要依賴一個存儲,當然如果你希望使用有狀態的 refresh token 也是可以自己擴展的,下面來看一些實現代碼
ITokenService 提供了 token 服務的抽象,定義如下:
public?interface?ITokenService {Task<TokenEntity>?GenerateToken(params?Claim[]?claims);Task<TokenValidationResult>?ValidateToken(string?token);Task<TokenEntity>?RefreshToken(string?refreshToken); }JwtTokenService 是基于 Jwt 的 Token 服務實現:
public?class?JwtTokenService?:?ITokenService {private?readonly?JwtSecurityTokenHandler?_tokenHandler?=?new();private?readonly?JwtTokenOptions?_tokenOptions;private?readonly?Lazy<TokenValidationParameters>_lazyTokenValidationParameters,_lazyRefreshTokenValidationParameters;public?JwtTokenService(IOptions<JwtTokenOptions>?tokenOptions){_tokenOptions?=?tokenOptions.Value;_lazyTokenValidationParameters?=?new(()?=>_tokenOptions.GetTokenValidationParameters());_lazyRefreshTokenValidationParameters?=?new(()?=>_tokenOptions.GetTokenValidationParameters(parameters?=>{parameters.ValidAudience?=?GetRefreshTokenAudience();}));}public?virtual?Task<TokenEntity>?GenerateToken(params?Claim[]?claims)=>?GenerateTokenInternal(_tokenOptions.EnableRefreshToken,?claims);public?virtual?Task<TokenValidationResult>?ValidateToken(string?token){return?_tokenHandler.ValidateTokenAsync(token,?_lazyTokenValidationParameters.Value);}public?virtual?async?Task<TokenEntity>?RefreshToken(string?refreshToken){var?refreshTokenValidateResult?=?await?_tokenHandler.ValidateTokenAsync(refreshToken,?_lazyRefreshTokenValidationParameters.Value);if?(!refreshTokenValidateResult.IsValid){throw?new?InvalidOperationException("Invalid?RefreshToken",?refreshTokenValidateResult.Exception);}return?await?GenerateTokenInternal(false,refreshTokenValidateResult.Claims.Where(x?=>?x.Key?!=?JwtRegisteredClaimNames.Jti).Select(c?=>?new?Claim(c.Key,?c.Value.ToString()????string.Empty)).ToArray());}protected?virtual?Task<string>?GetRefreshToken(Claim[]?claims,?string?jti){var?claimList?=?new?List<Claim>((claims????Array.Empty<Claim>()).Where(c?=>?c.Type?!=?_tokenOptions.RefreshTokenOwnerClaimType).Union(new[]?{?new?Claim(_tokenOptions.RefreshTokenOwnerClaimType,?jti)?}));claimList.RemoveAll(c?=>JwtInternalClaimTypes.Contains(c.Type)||?c.Type?==?JwtRegisteredClaimNames.Jti);var?jtiNew?=?_tokenOptions.JtiGenerator?.Invoke()????GuidIdGenerator.Instance.NewId();claimList.Add(new(JwtRegisteredClaimNames.Jti,?jtiNew));var?now?=?DateTimeOffset.UtcNow;claimList.Add(new?Claim(JwtRegisteredClaimNames.Iat,?now.ToUnixTimeMilliseconds().ToString(),?ClaimValueTypes.Integer64));var?jwt?=?new?JwtSecurityToken(issuer:?_tokenOptions.Issuer,audience:?GetRefreshTokenAudience(),claims:?claimList,notBefore:?now.UtcDateTime,expires:?now.Add(_tokenOptions.RefreshTokenValidFor).UtcDateTime,signingCredentials:?_tokenOptions.SigningCredentials);var?encodedJwt?=?_tokenHandler.WriteToken(jwt);return?encodedJwt.WrapTask();}private?static?readonly?HashSet<string>?JwtInternalClaimTypes?=?new(){"iss","exp","aud","nbf","iat"};private?async?Task<TokenEntity>?GenerateTokenInternal(bool?refreshToken,?Claim[]?claims){var?now?=?DateTimeOffset.UtcNow;var?claimList?=?new?List<Claim>(){new?(JwtRegisteredClaimNames.Iat,?now.ToUnixTimeMilliseconds().ToString(),?ClaimValueTypes.Integer64)};if?(claims?!=?null){claimList.AddRange(claims.Where(x?=>?!JwtInternalClaimTypes.Contains(x.Type)));}var?jti?=?claimList.FirstOrDefault(c?=>?c.Type?==?JwtRegisteredClaimNames.Jti)?.Value;if?(jti.IsNullOrEmpty()){jti?=?_tokenOptions.JtiGenerator?.Invoke()????GuidIdGenerator.Instance.NewId();claimList.Add(new(JwtRegisteredClaimNames.Jti,?jti));}var?jwt?=?new?JwtSecurityToken(issuer:?_tokenOptions.Issuer,audience:?_tokenOptions.Audience,claims:?claimList,notBefore:?now.UtcDateTime,expires:?now.Add(_tokenOptions.ValidFor).UtcDateTime,signingCredentials:?_tokenOptions.SigningCredentials);var?encodedJwt?=?_tokenHandler.WriteToken(jwt);var?response?=?refreshToken???new?TokenEntityWithRefreshToken(){AccessToken?=?encodedJwt,ExpiresIn?=?(int)_tokenOptions.ValidFor.TotalSeconds,RefreshToken?=?await?GetRefreshToken(claims,?jti)}?:?new?TokenEntity(){AccessToken?=?encodedJwt,ExpiresIn?=?(int)_tokenOptions.ValidFor.TotalSeconds};return?response;}private?string?GetRefreshTokenAudience()?=>?$"{_tokenOptions.Audience}_RefreshToken"; }在生成 refresh token 的時候會把關聯的 access token 的 jti(jwt token 的 id,默認是一個 guid 可以通過option 自定義)寫到 access token 中,claim type 可以通過 option 自定義,這樣如果想要實現 refresh token 所屬的 access token 的匹配校驗也是可以實現的。
生成 refresh token 的時候會把生成 access token 時的 claims 信息也會生成在 refresh token 中,這樣做的好處在于使用 refresh token 刷新 access token 的時候就可以直接根據 refresh token 生成 access token 無需別的信息,刷新得到的 access-token 中會有之前的 access token 的一個 id,如果想要記錄所有 token 的頒發過程也是可以實現的。
如果想要實現有狀態的 Refresh token 只需要重寫 JwtTokenService 中 GetRefreshToken 和 RefreshToken 兩個虛方法即可
Integration with JwtBearerAuth
如何和 asp.net core 的 JwtBearerAuthentication 進行集成呢?為了方便集成,提供了一個擴展來方便的集成,只需要使用 AddJwtTokenServiceWithJwtBearerAuth 來注冊即可,實現代碼如下:
public?static?IServiceCollection?AddJwtTokenServiceWithJwtBearerAuth(this?IServiceCollection?serviceCollection,?Action<JwtTokenOptions>?optionsAction,?Action<JwtBearerOptions>?jwtBearerOptionsSetup?=?null) {Guard.NotNull(serviceCollection);Guard.NotNull(optionsAction);if?(jwtBearerOptionsSetup?is?not?null){serviceCollection.Configure(jwtBearerOptionsSetup);}serviceCollection.ConfigureOptions<JwtBearerOptionsPostSetup>();return?serviceCollection.AddJwtTokenService(optionsAction); }JwtBearerOptionsPostSetup 實現如下:
internal?sealed?class?JwtBearerOptionsPostSetup?:IPostConfigureOptions<JwtBearerOptions> {private?readonly?IOptions<JwtTokenOptions>?_options;public?JwtBearerOptionsPostSetup(IOptions<JwtTokenOptions>?options){_options?=?options;}public?void?PostConfigure(string?name,?JwtBearerOptions?options){options.Audience?=?_options.Value.Audience;options.ClaimsIssuer?=?_options.Value.Issuer;options.TokenValidationParameters?=?_options.Value.GetTokenValidationParameters();} }JwtBearerOptionsPostSetup 主要就是配置的 JwtBearerOptions 的 TokenValidationParameters 以使用配置好的一些參數來進行驗證,避免了兩個地方都要配置
使用示例如下:
首先我們準備一個 API 來驗證 Auth 是否成功,API 很簡單,定義如下:
[HttpGet("[action]")] [Authorize(AuthenticationSchemes?=?"Bearer")] public?IActionResult?BearerAuthTest() {return?Ok(); }我們先獲取一個 access token,然后調用接口來驗證 Auth 能否成功
Bearer token testNo token
More
除了上面的示例,你也可以參考這個項目 https://github.com/WeihanLi/SparkTodo/tree/master/SparkTodo.API,之前獨立使用 Jwt token 的,現在也使用了上面的實現
目前的實現基于可以滿足我自己的需要了,還有一些可以優化的點
現在對于 refresh token 的校驗可以優化一下,目前只是驗證了一個 refresh token 的合法性,驗證 owner jwt token id 雖然可以實現,但是有些不太方便,可以優化一下
現在 refresh token 簽名用到的 key 和 access token 是同一個,應該允許用戶分開配置
使用 refresh token 獲取新的 token 時只返回 access token,可以支持返回新的 token 時返回 refresh_token
你覺得還有哪些需要改進的地方呢?
References
https://github.com/WeihanLi/SparkTodo
https://github.com/WeihanLi/SparkTodo/tree/master/SparkTodo.API
https://github.com/WeihanLi/WeihanLi.Web.Extensions
https://github.com/WeihanLi/WeihanLi.Web.Extensions/tree/dev/samples/WeihanLi.Web.Extensions.Samples
更輕易地實現 Jwt Token
總結
以上是生活随笔為你收集整理的Jwt Token 的刷新机制设计的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET Core中使用结果过滤器Res
- 下一篇: 更轻易地实现 Jwt Token