网站主页图片尺寸,有什么网站可以做微信支付宝支付,wordpress的滑块换成图片,四川营销网站建设Jwt Token 的刷新机制设计Intro前面的文章我们介绍了如何实现一个简单的 Jwt Server#xff0c;可以实现一个简单 Jwt 服务#xff0c;但是使用 Jwt token 会有一个缺点就是 token 一旦颁发就不能够进行作废#xff0c;所以通常 jwt token 的有效期一般会比较短#xff0c;… 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 TaskIActionResult 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 TaskIActionResult 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 TaskIActionResult ValidateToken(string token, [FromServices] ITokenService tokenService)
{return await tokenService.ValidateToken(token).ContinueWith(r r.Result.WrapResult().GetRestResult());
}验证步骤如下获取 tokenaccess tokenrefresh token验证 access token使用 refresh token 验证 token使用 refresh token 获取新的 access tokenrenew token with the refresh tokennew access token验证新的 access tokenvalidate token with the new access tokenImplement从上面 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
{TaskTokenEntity GenerateToken(params Claim[] claims);TaskTokenValidationResult ValidateToken(string token);TaskTokenEntity RefreshToken(string refreshToken);
}JwtTokenService 是基于 Jwt 的 Token 服务实现public class JwtTokenService : ITokenService
{private readonly JwtSecurityTokenHandler _tokenHandler new();private readonly JwtTokenOptions _tokenOptions;private readonly LazyTokenValidationParameters_lazyTokenValidationParameters,_lazyRefreshTokenValidationParameters;public JwtTokenService(IOptionsJwtTokenOptions tokenOptions){_tokenOptions tokenOptions.Value;_lazyTokenValidationParameters new(() _tokenOptions.GetTokenValidationParameters());_lazyRefreshTokenValidationParameters new(() _tokenOptions.GetTokenValidationParameters(parameters {parameters.ValidAudience GetRefreshTokenAudience();}));}public virtual TaskTokenEntity GenerateToken(params Claim[] claims) GenerateTokenInternal(_tokenOptions.EnableRefreshToken, claims);public virtual TaskTokenValidationResult ValidateToken(string token){return _tokenHandler.ValidateTokenAsync(token, _lazyTokenValidationParameters.Value);}public virtual async TaskTokenEntity 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 Taskstring GetRefreshToken(Claim[] claims, string jti){var claimList new ListClaim((claims ?? Array.EmptyClaim()).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 HashSetstring JwtInternalClaimTypes new(){iss,exp,aud,nbf,iat};private async TaskTokenEntity GenerateTokenInternal(bool refreshToken, Claim[] claims){var now DateTimeOffset.UtcNow;var claimList new ListClaim(){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 的 jtijwt 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, ActionJwtTokenOptions optionsAction, ActionJwtBearerOptions jwtBearerOptionsSetup null)
{Guard.NotNull(serviceCollection);Guard.NotNull(optionsAction);if (jwtBearerOptionsSetup is not null){serviceCollection.Configure(jwtBearerOptionsSetup);}serviceCollection.ConfigureOptionsJwtBearerOptionsPostSetup();return serviceCollection.AddJwtTokenService(optionsAction);
}JwtBearerOptionsPostSetup 实现如下internal sealed class JwtBearerOptionsPostSetup :IPostConfigureOptionsJwtBearerOptions
{private readonly IOptionsJwtTokenOptions _options;public JwtBearerOptionsPostSetup(IOptionsJwtTokenOptions 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 tokenMore除了上面的示例你也可以参考这个项目 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你觉得还有哪些需要改进的地方呢Referenceshttps://github.com/WeihanLi/SparkTodohttps://github.com/WeihanLi/SparkTodo/tree/master/SparkTodo.APIhttps://github.com/WeihanLi/WeihanLi.Web.Extensionshttps://github.com/WeihanLi/WeihanLi.Web.Extensions/tree/dev/samples/WeihanLi.Web.Extensions.Samples更轻易地实现 Jwt Token