后来笔者的做法是:当客户端每次发起Http请求时,先判断本地Token是否存在:
1. 如果不存在,则先向服务端发起登录验证请求,从而获取Token。
2. 如果已存在,则检测Token是否即将过期。如果是的话,就重新发起登录验证更新Token,否则继续使用当前Token。其中判断Token是否即将过期没有一个标准设定,个人认为在1~5分钟之间比较合适。 以上就是实现Token自动续期的整个过程。
public record TokenResult { /// <summary> /// 访问令牌 /// </summary> public string AccessToken { get; init; } /// <summary> /// 过期时间 /// </summary> public DateTime ExpiredTime { get; init; } }服务端实现
public class Program { public static void Main(string[] args) { // 堆代码 duidaima.com var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(o => { o.TokenValidationParameters = new TokenValidationParameters { NameClaimType = "Name", RoleClaimType = "Role", ValidateAudience = false, ValidateIssuer = false, ValidateLifetime = true, ClockSkew = TimeSpan.FromSeconds(30), IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtConsts.SigningKey)) }; }); builder.Services.AddAuthorization(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run(); } }DemoController 控制器:提供 LoginAsync() 和 GetCurrentTimeAsync() 两个方法,代码如下:
[ApiController] [Route("[controller]")] public class DemoController : ControllerBase { /// <summary> /// 登录 /// </summary> /// <param name="dto"></param> /// <returns></returns> [HttpPost("Login")] public async ValueTask<TokenResult> LoginAsync(LoginDto dto) { var user = GetUserInfo(dto.UserName); if (user.Password == dto.Password) // 登录密码验证 { TokenResult tokenResult = await JwtHelper.GenerateAsync(user.Id, user.UserName, user.Name, user.PhoneNumber); return tokenResult; } return null; } /// <summary> /// 获取当前时间 /// </summary> /// <returns></returns> [Authorize] [HttpGet("CurrentTime")] public ValueTask<DateTimeOffset> GetCurrentTimeAsync() { return ValueTask.FromResult(DateTimeOffset.Now); } }第26行代码:给 GetCurrentTimeAsync() 加上 [Authorize] 特性后, 当前服务必须授权后才能访问。
public static class JwtHelper { /// <summary> /// 生成Token /// </summary> /// <returns></returns> public static ValueTask<TokenResult> GenerateAsync(int id, string username, string name, string phoneNumber) { var claims = new List<Claim>() { new Claim("UserId", id.ToString()), // 用户Id new Claim("UserName", username), // 用户名 new Claim("Name", name) , // 姓名 new Claim("PhoneNumber", phoneNumber) // 手机号码 }; var tokenHandler = new JwtSecurityTokenHandler(); var expiresAt = DateTime.Now.AddMinutes(20); // 过期时间 var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(claims), Expires = expiresAt, SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes(JwtConsts.SigningKey)), SecurityAlgorithms.HmacSha256Signature) }; var token = tokenHandler.CreateToken(tokenDescriptor); var tokenString = tokenHandler.WriteToken(token); return ValueTask.FromResult(new TokenResult { AccessToken = tokenString, ExpiredTime = expiresAt }); } }第18行代码:设置Token的过期时间,这里我们把有效期设为20分钟。
[Headers(new[] { "Authorization:Bearer" })] public interface IDemoApi { /// <summary> /// 获取当前时间 /// </summary> /// <returns></returns> [Get("/Demo/CurrentTime")] Task<DateTimeOffset> GetCurrentTimeAsync(); }第1行代码:给 IDemApi 接口加上 [Headers(...)] 特性,这样每次调用 GetCurrentTimeAsync() 方法,Http请求头部都会携带此信息。JWT的标准头部格式为:Authorization: Bearer <token>。接下来,就是实现Token自动续期功能。笔者封装了一个 RestHelper 类,核心代码如下:
/// <summary> /// Rest请求服务 /// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> public static T For<T>() { var settings = new RefitSettings() { AuthorizationHeaderValueGetter = () => GetTokenAsync(), }; return RestService.For<T>(BaseUrl, settings); } /// <summary> /// 获取Token /// </summary> /// <returns></returns> private static async Task<string> GetTokenAsync() { if (TokenResult is null || DateTimeOffset.Now.AddMinutes(1) >= TokenResult?.ExpiredTime) { var uri = new Uri($"{BaseUrl}/demo/login", UriKind.Absolute); var dto = new LoginDto { UserName = "fjq", Password = "123456" }; using var httpResMsg = await new HttpClient().PostAsync(uri, JsonContent.Create(dto)); if (httpResMsg.IsSuccessStatusCode) { var jsonStr = await httpResMsg.Content.ReadAsStringAsync(); TokenResult = JsonHelper.FromJson<TokenResult>(jsonStr); } } return TokenResult?.AccessToken; }第10行代码:AuthorizationHeaderValueGetter 是 RefitSettings 对象的一个委托属性,用来提供授权头部信息,即JWT字符串。
var dt = await RestHelper.For<IDemoApi>().GetCurrentTimeAsync();
界面运行效果如下(亲测有效):