日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Jwt Token 的刷新机制设计

發布時間:2023/12/4 编程问答 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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

access token

refresh?token


  • 驗證 access token

  • 使用 refresh token 驗證 token

  • 使用 refresh token 獲取新的 access token

renew token with the refresh token

new access token
  • 驗證新的 access token

validate token with the new 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 test

No 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 的刷新机制设计的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。