前言 筆者之前開發過一套C/S架構的桌面應用,採用了JWT作為用戶的登錄認證和授權。遇到的唯一問題就是JWT過期了該怎麼辦?設想當一個用戶正在進行業務操作,突然因為Token過期失效,莫名其妙地跳轉到登錄界面,是不是一件很無語的事。當然筆者也曾想過:為何不把JWT的有效期儘量設長些(假設24小時), ...
前言
筆者之前開發過一套C/S架構的桌面應用,採用了JWT作為用戶的登錄認證和授權。遇到的唯一問題就是JWT過期了該怎麼辦?設想當一個用戶正在進行業務操作,突然因為Token過期失效,莫名其妙地跳轉到登錄界面,是不是一件很無語的事。當然筆者也曾想過:為何不把JWT的有效期儘量設長些(假設24小時),用戶每天總要下班退出系統吧,呵呵!這顯然有點投機取巧,也違背了JWT的安全設計,看來等另想他法。
設計思路
後來筆者的做法是:當客戶端每次發起Http請求時,先判斷本地Token是否存在: 1. 如果不存在,則先向服務端發起登錄驗證請求,從而獲取Token。2. 如果已存在,則檢測Token是否即將過期。如果是的話,就重新發起登錄驗證更新Token,否則繼續使用當前Token。其中判斷Token是否即將過期沒有一個標準設定,個人認為在1~5分鐘之間比較合適。 以上就是實現Token自動續期的整個過程。
知識準備
什麼是JWT
JWT(JSON Web Token) 是一個開發標準 (RFC 7519),它定義了一種緊湊的、自包含的方式,用於作為JSON對象在各方之間安全地傳輸信息。該信息可以被驗證和信任,因為它是數字簽名的。JWT是由頭部 (Header)、載荷 (Payload) 和簽名 (Signature) 三部分組成,它們之間用圓點(.)連接。JWT最常見的應用場景是授權(Authorization)和信息交換(Information Exchange)。
什麼是Refit
Refit 是一個受到Square的Retrofit庫(Java)啟發的自動類型安全REST庫。我們的應用程式通過Refit請求網路,實際上是使用Refit介面層封裝請求參數、Header、Url等信息,之後由HttpClient完成後續的請求操作,在服務端返回數據之後,HttpClient將原始的結果交給Refit,後者根據用戶的需求對結果進行解析的過程。
技術實現
我們需要先創建一個客戶端和一個服務端。為了演示方便,客戶端仍用WinForm,伺服器使用ASP.NET Core Web API。如圖所示:
JwtToken.Shared 公共類庫:定義了一些POCO對象,供客戶端/服務端共用使用。其中 TokenResult 定義如下:
1 public record TokenResult 2 { 3 /// <summary> 4 /// 訪問令牌 5 /// </summary> 6 public string AccessToken { get; init; } 7 8 /// <summary> 9 /// 過期時間 10 /// </summary> 11 public DateTime ExpiredTime { get; init; } 12 }
服務端實現
JwtToken.Server 提供兩個後臺服務:一個是登錄驗證服務,為客戶端頒發用戶憑證(JWT),另一個是獲取系統時間服務。
在 Program 啟動類,我們需要添加和使用指定服務,從而開啟JWT認證和授權。 代碼如下:
1 public class Program 2 { 3 public static void Main(string[] args) 4 { 5 var builder = WebApplication.CreateBuilder(args); 6 builder.Services.AddControllers(); 7 builder.Services.AddAuthentication(options => 8 { 9 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 10 options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 11 }) 12 .AddJwtBearer(o => 13 { 14 o.TokenValidationParameters = new TokenValidationParameters 15 { 16 NameClaimType = "Name", 17 RoleClaimType = "Role", 18 ValidateAudience = false, 19 ValidateIssuer = false, 20 ValidateLifetime = true, 21 ClockSkew = TimeSpan.FromSeconds(30), 22 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtConsts.SigningKey)) 23 }; 24 }); 25 builder.Services.AddAuthorization(); 26 27 var app = builder.Build(); 28 app.UseAuthentication(); 29 app.UseAuthorization(); 30 app.MapControllers(); 31 app.Run(); 32 } 33 }
DemoController 控制器:提供 LoginAsync() 和 GetCurrentTimeAsync() 兩個方法,代碼如下:
1 [ApiController] 2 [Route("[controller]")] 3 public class DemoController : ControllerBase 4 { 5 /// <summary> 6 /// 登錄 7 /// </summary> 8 /// <param name="dto"></param> 9 /// <returns></returns> 10 [HttpPost("Login")] 11 public async ValueTask<TokenResult> LoginAsync(LoginDto dto) 12 { 13 var user = GetUserInfo(dto.UserName); 14 if (user.Password == dto.Password) // 登錄密碼驗證 15 { 16 TokenResult tokenResult = await JwtHelper.GenerateAsync(user.Id, user.UserName, user.Name, user.PhoneNumber); 17 return tokenResult; 18 } 19 return null; 20 } 21 22 /// <summary> 23 /// 獲取當前時間 24 /// </summary> 25 /// <returns></returns> 26 [Authorize] 27 [HttpGet("CurrentTime")] 28 public ValueTask<DateTimeOffset> GetCurrentTimeAsync() 29 { 30 return ValueTask.FromResult(DateTimeOffset.Now); 31 } 32 }
第26行代碼:給 GetCurrentTimeAsync() 加上 [Authorize] 特性後, 當前服務必須授權後才能訪問。
第16行代碼:根據用戶的Id、用戶名、姓名等信息來生成 TokenResult ,它包含JWT令牌和過期時間。下麵是JWT的生成代碼:
1 public static class JwtHelper 2 { 3 /// <summary> 4 /// 生成Token 5 /// </summary> 6 /// <returns></returns> 7 public static ValueTask<TokenResult> GenerateAsync(int id, string username, string name, string phoneNumber) 8 { 9 var claims = new List<Claim>() 10 { 11 new Claim("UserId", id.ToString()), // 用戶Id 12 new Claim("UserName", username), // 用戶名 13 new Claim("Name", name) , // 姓名 14 new Claim("PhoneNumber", phoneNumber) // 手機號碼 15 }; 16 17 var tokenHandler = new JwtSecurityTokenHandler(); 18 var expiresAt = DateTime.Now.AddMinutes(20); // 過期時間 19 var tokenDescriptor = new SecurityTokenDescriptor 20 { 21 Subject = new ClaimsIdentity(claims), 22 Expires = expiresAt, 23 SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes(JwtConsts.SigningKey)), 24 SecurityAlgorithms.HmacSha256Signature) 25 }; 26 27 var token = tokenHandler.CreateToken(tokenDescriptor); 28 var tokenString = tokenHandler.WriteToken(token); 29 30 return ValueTask.FromResult(new TokenResult 31 { 32 AccessToken = tokenString, 33 ExpiredTime = expiresAt 34 }); 35 } 36 }
第18行代碼:設置Token的過期時間,這裡我們把有效期設為20分鐘。
客戶端實現
JwtToken.Client 定義後臺服務調用介面和實現Token自動續期。IDemoApi 介面定義如下:
1 [Headers(new[] { "Authorization:Bearer" })] 2 public interface IDemoApi 3 { 4 /// <summary> 5 /// 獲取當前時間 6 /// </summary> 7 /// <returns></returns> 8 [Get("/Demo/CurrentTime")] 9 Task<DateTimeOffset> GetCurrentTimeAsync(); 10 }
第1行代碼:給 IDemApi 介面加上 [Headers(...)] 特性,這樣每次調用 GetCurrentTimeAsync() 方法,Http請求頭部都會加上此信息。JWT的標準授權頭部格式為:Authorization: Bearer <token>。
接下來,就是實現Token自動續期功能。筆者封裝了一個 RestHelper 類,核心代碼如下:
1 /// <summary> 2 /// Rest請求服務 3 /// </summary> 4 /// <typeparam name="T"></typeparam> 5 /// <returns></returns> 6 public static T For<T>() 7 { 8 var settings = new RefitSettings() 9 { 10 AuthorizationHeaderValueGetter = () => GetTokenAsync(), 11 }; 12 13 return RestService.For<T>(BaseUrl, settings); 14 } 15 16 /// <summary> 17 /// 獲取Token 18 /// </summary> 19 /// <returns></returns> 20 private static async Task<string> GetTokenAsync() 21 { 22 if (TokenResult is null || DateTimeOffset.Now.AddMinutes(1) >= TokenResult?.ExpiredTime) 23 { 24 var uri = new Uri($"{BaseUrl}/demo/login", UriKind.Absolute); 25 26 var dto = new LoginDto { UserName = "fjq", Password = "123456" }; 27 28 using var httpResMsg = await new HttpClient().PostAsync(uri, JsonContent.Create(dto)); 29 30 if (httpResMsg.IsSuccessStatusCode) 31 { 32 var jsonStr = await httpResMsg.Content.ReadAsStringAsync(); 33 34 TokenResult = JsonHelper.FromJson<TokenResult>(jsonStr); 35 } 36 } 37 38 return TokenResult?.AccessToken; 39 }
第10行代碼:AuthorizationHeaderValueGetter 是 RefitSettings 對象的一個委托屬性,用來提供授權頭部信息,即JWT字元串。
第22至35行代碼:即按照筆者前面的思路轉換成代碼實現,這裡就不再詳細說明瞭。
最後,我們用一行代碼來獲取後臺系統時間:
1 var dt = await RestHelper.For<IDemoApi>().GetCurrentTimeAsync();
界面運行效果如下(~親測有效~):
參考資料
認識JWT - 廢物大師兄 - 博客園 (cnblogs.com)
Refit | The automatic type-safe REST library for Xamarin and .NET (reactiveui.github.io)
作者:天行健君子以自強 出處:https://www.cnblogs.com/fengjq/p/17631841.html 如果此文對你有幫助的話,請點一下右下角的【推薦】,歡迎評論區留言。本文已同步至作者微信公眾號:玩轉DotNet,感謝掃碼關註!