C#利用Refit實(shí)現(xiàn)JWT自動(dòng)續(xù)期詳解
前言
筆者之前開發(fā)過一套C/S架構(gòu)的桌面應(yīng)用,采用了JWT作為用戶的登錄認(rèn)證和授權(quán)。遇到的唯一問題就是JWT過期了該怎么辦?設(shè)想當(dāng)一個(gè)用戶正在進(jìn)行業(yè)務(wù)操作,突然因?yàn)門oken過期失效,莫名其妙地跳轉(zhuǎn)到登錄界面,是不是一件很無語的事。當(dāng)然筆者也曾想過:為何不把JWT的有效期盡量設(shè)長些(假設(shè)24小時(shí)),用戶每天總要下班退出系統(tǒng)吧,呵呵!這顯然有點(diǎn)投機(jī)取巧,也違背了JWT的安全設(shè)計(jì),看來等另想它法。
設(shè)計(jì)思路
后來筆者的做法是:當(dāng)客戶端每次發(fā)起Http請求時(shí),先判斷本地Token是否存在: 1. 如果不存在,則先向服務(wù)端發(fā)起登錄驗(yàn)證請求,從而獲取Token。2. 如果已存在,則檢測Token是否即將過期。如果是的話,就重新發(fā)起登錄驗(yàn)證更新Token,否則繼續(xù)使用當(dāng)前Token。其中判斷Token是否即將過期沒有一個(gè)標(biāo)準(zhǔn)設(shè)定,個(gè)人認(rèn)為在1~5分鐘之間比較合適。 以上就是實(shí)現(xiàn)Token自動(dòng)續(xù)期的整個(gè)過程。
知識(shí)準(zhǔn)備
什么是JWT
JWT(JSON Web Token) 是一個(gè)開發(fā)標(biāo)準(zhǔn) (RFC 7519),它定義了一種緊湊的、自包含的方式,用于作為JSON對象在各方之間安全地傳輸信息。該信息可以被驗(yàn)證和信任,因?yàn)樗菙?shù)字簽名的。JWT是由頭部 (Header)、載荷 (Payload) 和簽名 (Signature) 三部分組成,它們之間用圓點(diǎn)(.)連接。JWT最常見的應(yīng)用場景是授權(quán)(Authorization)和信息交換(Information Exchange)。
什么是Refit
Refit 是一個(gè)受到Square的Retrofit庫(Java)啟發(fā)的自動(dòng)類型安全REST庫。我們的應(yīng)用程序通過Refit請求網(wǎng)絡(luò),實(shí)際上是使用Refit接口層封裝請求參數(shù)、Header、Url等信息,之后由HttpClient完成后續(xù)的請求操作,在服務(wù)端返回?cái)?shù)據(jù)之后,HttpClient將原始的結(jié)果交給Refit,后者根據(jù)用戶的需求對結(jié)果進(jìn)行解析的過程。
技術(shù)實(shí)現(xiàn)
我們需要先創(chuàng)建客戶端和服務(wù)端。為了演示方便,客戶端仍用WinForm,服務(wù)器使用ASP.NET Core Web API。如圖所示:
JwtToken.Shared 公共類庫:定義了一些POCO對象,供客戶端/服務(wù)端共享使用。其中 TokenResult 定義如下:
public record TokenResult { /// <summary> /// 訪問令牌 /// </summary> public string AccessToken { get; init; } /// <summary> /// 過期時(shí)間 /// </summary> public DateTime ExpiredTime { get; init; } }
服務(wù)端實(shí)現(xiàn)
JwtToken.Server 提供兩個(gè)后臺(tái)服務(wù):一個(gè)是登錄驗(yàn)證服務(wù),為客戶端頒發(fā)用戶憑證(JWT),另一個(gè)是獲取系統(tǒng)時(shí)間服務(wù)。
在 Program 啟動(dòng)類,我們需要添加和使用指定服務(wù),從而開啟JWT認(rèn)證和授權(quán)。 代碼如下:
public class Program { public static void Main(string[] args) { 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() 兩個(gè)方法,代碼如下:
[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) // 登錄密碼驗(yàn)證 { TokenResult tokenResult = await JwtHelper.GenerateAsync(user.Id, user.UserName, user.Name, user.PhoneNumber); return tokenResult; } return null; } /// <summary> /// 獲取當(dāng)前時(shí)間 /// </summary> /// <returns></returns> [Authorize] [HttpGet("CurrentTime")] public ValueTask<DateTimeOffset> GetCurrentTimeAsync() { return ValueTask.FromResult(DateTimeOffset.Now); } }
第26行代碼:給 GetCurrentTimeAsync() 加上 [Authorize] 特性后, 當(dāng)前服務(wù)必須授權(quán)后才能訪問。
第16行代碼:根據(jù)用戶的Id、用戶名、姓名等信息來生成 TokenResult ,它包含JWT令牌和過期時(shí)間。下面是JWT的生成代碼:
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) // 手機(jī)號碼 }; var tokenHandler = new JwtSecurityTokenHandler(); var expiresAt = DateTime.Now.AddMinutes(20); // 過期時(shí)間 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行代碼:設(shè)置Token的過期時(shí)間,這里我們把有效期設(shè)為20分鐘。
客戶端實(shí)現(xiàn)
JwtToken.Client 定義后臺(tái)服務(wù)調(diào)用接口和實(shí)現(xiàn)Token自動(dòng)續(xù)期。IDemoApi 接口定義如下:
[Headers(new[] { "Authorization:Bearer" })] public interface IDemoApi { /// <summary> /// 獲取當(dāng)前時(shí)間 /// </summary> /// <returns></returns> [Get("/Demo/CurrentTime")] Task<DateTimeOffset> GetCurrentTimeAsync(); }
第1行代碼:給 IDemApi 接口加上 [Headers(...)] 特性,這樣每次調(diào)用 GetCurrentTimeAsync() 方法,Http請求頭部都會(huì)攜帶此信息。JWT的標(biāo)準(zhǔn)頭部格式為:Authorization: Bearer <token>。
接下來,就是實(shí)現(xiàn)Token自動(dòng)續(xù)期功能。筆者封裝了一個(gè) RestHelper 類,核心代碼如下:
/// <summary> /// Rest請求服務(wù) /// </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 對象的一個(gè)委托屬性,用來提供授權(quán)頭部信息,即JWT字符串。
第22至35行代碼:即按照筆者前面的思路轉(zhuǎn)換成代碼,這里就不多介紹了。
最后,我們通過下面一行代碼來獲取后臺(tái)系統(tǒng)時(shí)間:
var dt = await RestHelper.For<IDemoApi>().GetCurrentTimeAsync();
界面運(yùn)行效果如下(親測有效):
到此這篇關(guān)于C#利用Refit實(shí)現(xiàn)JWT自動(dòng)續(xù)期詳解的文章就介紹到這了,更多相關(guān)C# Refit實(shí)現(xiàn)JWT自動(dòng)續(xù)期內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#使用foreach遍歷哈希表(hashtable)的方法
這篇文章主要介紹了C#使用foreach遍歷哈希表(hashtable)的方法,是C#中foreach語句遍歷散列表的典型應(yīng)用,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-04-04C#實(shí)現(xiàn)XML與實(shí)體類之間相互轉(zhuǎn)換的方法(序列化與反序列化)
這篇文章主要介紹了C#實(shí)現(xiàn)XML與實(shí)體類之間相互轉(zhuǎn)換的方法,涉及C#序列化與反序列化操作的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2016-06-06C# 修改文件的創(chuàng)建、修改和訪問時(shí)間的示例
這篇文章主要介紹了C#實(shí)現(xiàn)修改文件的創(chuàng)建、修改和訪問時(shí)間的示例,幫助大家更好的理解和學(xué)習(xí)使用c#,感興趣的朋友可以了解下2021-04-04C#使用Exchange實(shí)現(xiàn)發(fā)送郵件
最近項(xiàng)目中需要用到exchange的操作,所以本文就參照msdn弄了一個(gè)簡單的C#操作類,實(shí)現(xiàn)了發(fā)送郵件和拉取收件箱的功能,感興趣的小伙伴可以了解下2023-10-10C#使用XSLT實(shí)現(xiàn)xsl、xml與html相互轉(zhuǎn)換
這篇文章介紹了C#使用XSLT實(shí)現(xiàn)xsl、xml與html相互轉(zhuǎn)換的方法,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06