Jwt Token 的刷新机制设计
Jwt Token 的刷新機(jī)制設(shè)計(jì)
Intro
前面的文章我們介紹了如何實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 Jwt Server,可以實(shí)現(xiàn)一個(gè)簡(jiǎn)單 Jwt 服務(wù),但是使用 Jwt token 會(huì)有一個(gè)缺點(diǎn)就是 token 一旦頒發(fā)就不能夠進(jìn)行作廢,所以通常 jwt token 的有效期一般會(huì)比較短,但是太短了又會(huì)比較影響用戶的用戶體驗(yàn),所以就有了 refresh token 的參與,一般來(lái)說(shuō) refresh token 會(huì)比實(shí)際用的 access token 有效期會(huì)長(zhǎng)一些,當(dāng) access token 失效了,就使用 refresh token 重新獲取一個(gè) access token,再使用新的 access_token 來(lái)訪問(wèn)服務(wù)。
Sample
我們的示例在前面的基礎(chǔ)上增加了 refresh_token,使用示例如下:
注冊(cè)服務(wù)的時(shí)候啟用 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 的時(shí)候就會(huì)返回一個(gè)帶著 refresh token 的 token 對(duì)象(TokenEntityWithRefreshToken) 否則就是返回只有 acess token 的對(duì)象 (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 來(lái)獲取新的 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 接口和上次的示例相比稍微有一些改動(dòng),主要是體現(xiàn)了有沒(méi)有 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()); }驗(yàn)證步驟如下:
獲取 token
refresh?token
驗(yàn)證 access token
使用 refresh token 驗(yàn)證 token
使用 refresh token 獲取新的 access token
new access token
驗(yàn)證新的 access token
Implement
從上面 token 解析出來(lái)的內(nèi)容大概可以看的出來(lái)實(shí)現(xiàn)的思路,我的實(shí)現(xiàn)思路是仍然使用 Jwt 這套機(jī)制來(lái)生成和驗(yàn)證 refresh token,只是 refresh token 的 audience 和 access token 不同,另外 refresh token 的有效期一般會(huì)更長(zhǎng)一些,這樣我們就不能把 refresh token 直接當(dāng)作 access token 來(lái)使用,因?yàn)?token 驗(yàn)證會(huì)失敗,而之所以利用 Jwt 的機(jī)制來(lái)實(shí)現(xiàn)也是希望能夠簡(jiǎn)化 refresh token,利用 jwt 的無(wú)狀態(tài),不需要使得無(wú)狀態(tài)的應(yīng)用變得有狀態(tài),有看過(guò)一些別的實(shí)現(xiàn)是直接使用存儲(chǔ)將 refresh token 保存起來(lái),這樣 refresh token 就變成有狀態(tài)的了,還要依賴一個(gè)存儲(chǔ),當(dāng)然如果你希望使用有狀態(tài)的 refresh token 也是可以自己擴(kuò)展的,下面來(lái)看一些實(shí)現(xiàn)代碼
ITokenService 提供了 token 服務(wù)的抽象,定義如下:
public?interface?ITokenService {Task<TokenEntity>?GenerateToken(params?Claim[]?claims);Task<TokenValidationResult>?ValidateToken(string?token);Task<TokenEntity>?RefreshToken(string?refreshToken); }JwtTokenService 是基于 Jwt 的 Token 服務(wù)實(shí)現(xiàn):
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 的時(shí)候會(huì)把關(guān)聯(lián)的 access token 的 jti(jwt token 的 id,默認(rèn)是一個(gè) guid 可以通過(guò)option 自定義)寫到 access token 中,claim type 可以通過(guò) option 自定義,這樣如果想要實(shí)現(xiàn) refresh token 所屬的 access token 的匹配校驗(yàn)也是可以實(shí)現(xiàn)的。
生成 refresh token 的時(shí)候會(huì)把生成 access token 時(shí)的 claims 信息也會(huì)生成在 refresh token 中,這樣做的好處在于使用 refresh token 刷新 access token 的時(shí)候就可以直接根據(jù) refresh token 生成 access token 無(wú)需別的信息,刷新得到的 access-token 中會(huì)有之前的 access token 的一個(gè) id,如果想要記錄所有 token 的頒發(fā)過(guò)程也是可以實(shí)現(xiàn)的。
如果想要實(shí)現(xiàn)有狀態(tài)的 Refresh token 只需要重寫 JwtTokenService 中 GetRefreshToken 和 RefreshToken 兩個(gè)虛方法即可
Integration with JwtBearerAuth
如何和 asp.net core 的 JwtBearerAuthentication 進(jìn)行集成呢?為了方便集成,提供了一個(gè)擴(kuò)展來(lái)方便的集成,只需要使用 AddJwtTokenServiceWithJwtBearerAuth 來(lái)注冊(cè)即可,實(shí)現(xiàn)代碼如下:
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 實(shí)現(xiàn)如下:
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 以使用配置好的一些參數(shù)來(lái)進(jìn)行驗(yàn)證,避免了兩個(gè)地方都要配置
使用示例如下:
首先我們準(zhǔn)備一個(gè) API 來(lái)驗(yàn)證 Auth 是否成功,API 很簡(jiǎn)單,定義如下:
[HttpGet("[action]")] [Authorize(AuthenticationSchemes?=?"Bearer")] public?IActionResult?BearerAuthTest() {return?Ok(); }我們先獲取一個(gè) access token,然后調(diào)用接口來(lái)驗(yàn)證 Auth 能否成功
Bearer token testNo token
More
除了上面的示例,你也可以參考這個(gè)項(xiàng)目 https://github.com/WeihanLi/SparkTodo/tree/master/SparkTodo.API,之前獨(dú)立使用 Jwt token 的,現(xiàn)在也使用了上面的實(shí)現(xiàn)
目前的實(shí)現(xiàn)基于可以滿足我自己的需要了,還有一些可以優(yōu)化的點(diǎn)
現(xiàn)在對(duì)于 refresh token 的校驗(yàn)可以優(yōu)化一下,目前只是驗(yàn)證了一個(gè) refresh token 的合法性,驗(yàn)證 owner jwt token id 雖然可以實(shí)現(xiàn),但是有些不太方便,可以優(yōu)化一下
現(xiàn)在 refresh token 簽名用到的 key 和 access token 是同一個(gè),應(yīng)該允許用戶分開配置
使用 refresh token 獲取新的 token 時(shí)只返回 access token,可以支持返回新的 token 時(shí)返回 refresh_token
你覺(jué)得還有哪些需要改進(jìn)的地方呢?
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
更輕易地實(shí)現(xiàn) Jwt Token
總結(jié)
以上是生活随笔為你收集整理的Jwt Token 的刷新机制设计的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: .NET Core中使用结果过滤器Res
- 下一篇: 更轻易地实现 Jwt Token