C#利用Refit實現JWT自動續期

来源:https://www.cnblogs.com/fengjq/archive/2023/08/18/17631841.html
-Advertisement-
Play Games

前言 筆者之前開發過一套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,感謝掃碼關註!
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這次介紹的小技巧不是統計,而是把統計結果作為**新列**和原來的數據放在一起。`pandas`的各種統計功能之前已經介紹了不少,但是每次都是統計結果歸統計結果,原始數據歸原始數據,沒有把它們合併在一個數據集中來觀察。 下麵通過兩個場景示例來演示如果把統計值作為新列的數據。 # 1. 成績統計的場景 ...
  • 在日常的開發過程中,我們不可避免地會使用到 JDK8 之前的 Date 類,在格式化日期或解析日期時就需要用到 SimpleDateFormat 類,但由於該類並不是線程安全的,所以我們常發現對該類的不恰當使用會導致日期解析異常,從而影響線上服務可用率。 ...
  • 本文通過簡單的示例代碼和說明,讓讀者能夠瞭解Mybatis-Plus+Nacos+Dubbo進行遠程RPC調用的簡單使用 預設你已經看過我之前的教程了,並且擁有上個教程完成的項目, 之前的教程 https://www.cnblogs.com/leafstar/p/17638782.html 項目鏈接 ...
  • 上一篇寫服務端的文章《MQTTnet4入門(一)實現服務端》已經是去年年底,現在MQTTnet的版本是4.2.1.781,總的來說改動不大。下麵以新版為例實現一個客戶端。 var mqttClientOptions = new MqttClientOptionsBuilder() .WithTcpS ...
  • 前段時間根據 [老張的哲學] 大佬講解的視頻做的筆記,講的很不錯。此文主要記錄JWT/DI依賴註入/AOP面向切麵編程/DTO/解決跨域等相關知識,還包含一些.NET Core項目實戰的一些案例。我是西瓜程式猿,感謝大家的支持! ...
  • 言: 今天在寫一個功能,需要接收演算法發過來的檢測數據,我這邊需要和演算法同步開發,有些地方需要演算法那邊的變數或數據,就寫了一些臨時變數,但是演算法對接後有需要把這些臨時變數改回來,所以想到了使用todo來記錄一下,等到代碼合併時把記錄的點修改過來。 接下來進行簡單的講解:C#中的todo就相當於我們的書 ...
  • 已知一張二維碼圖片,怎麼生成一張一模一樣的圖片出來? 最近有個項目,需要用到QRCode,之前只做過Datamatrix格式的,想著應該也是差不多的,於是就依葫蘆畫瓢,掏出我的陳年OnBarcode類庫,一通修改,生成了個嶄新的QRCode,與客戶提供的二維碼圖片一比對,雖然掃出來內容一樣,但明顯圖 ...
  • **簡介** 有時候我們在發佈程式時,不想讓客戶看到項目中的文件,這時就可以使用.NET的嵌入文件功能(虛擬文件)。在.NET中,虛擬文件(Virtual File)是一種特殊類型的文件,它們在編譯時被嵌入到程式集中,而不是作為獨立的文件存在於文件系統中。EmbeddedFile是指在編譯時將文件內 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...