C#利用Refit實現(xiàn)JWT自動續(xù)期詳解
前言
筆者之前開發(fā)過一套C/S架構(gòu)的桌面應(yīng)用,采用了JWT作為用戶的登錄認(rèn)證和授權(quán)。遇到的唯一問題就是JWT過期了該怎么辦?設(shè)想當(dāng)一個用戶正在進(jìn)行業(yè)務(wù)操作,突然因為Token過期失效,莫名其妙地跳轉(zhuǎn)到登錄界面,是不是一件很無語的事。當(dāng)然筆者也曾想過:為何不把JWT的有效期盡量設(shè)長些(假設(shè)24小時),用戶每天總要下班退出系統(tǒng)吧,呵呵!這顯然有點投機取巧,也違背了JWT的安全設(shè)計,看來等另想它法。
設(shè)計思路
后來筆者的做法是:當(dāng)客戶端每次發(fā)起Http請求時,先判斷本地Token是否存在: 1. 如果不存在,則先向服務(wù)端發(fā)起登錄驗證請求,從而獲取Token。2. 如果已存在,則檢測Token是否即將過期。如果是的話,就重新發(fā)起登錄驗證更新Token,否則繼續(xù)使用當(dāng)前Token。其中判斷Token是否即將過期沒有一個標(biāo)準(zhǔn)設(shè)定,個人認(rèn)為在1~5分鐘之間比較合適。 以上就是實現(xiàn)Token自動續(xù)期的整個過程。
知識準(zhǔn)備
什么是JWT
JWT(JSON Web Token) 是一個開發(fā)標(biāo)準(zhǔn) (RFC 7519),它定義了一種緊湊的、自包含的方式,用于作為JSON對象在各方之間安全地傳輸信息。該信息可以被驗證和信任,因為它是數(shù)字簽名的。JWT是由頭部 (Header)、載荷 (Payload) 和簽名 (Signature) 三部分組成,它們之間用圓點(.)連接。JWT最常見的應(yīng)用場景是授權(quán)(Authorization)和信息交換(Information Exchange)。
什么是Refit
Refit 是一個受到Square的Retrofit庫(Java)啟發(fā)的自動類型安全REST庫。我們的應(yīng)用程序通過Refit請求網(wǎng)絡(luò),實際上是使用Refit接口層封裝請求參數(shù)、Header、Url等信息,之后由HttpClient完成后續(xù)的請求操作,在服務(wù)端返回數(shù)據(jù)之后,HttpClient將原始的結(jié)果交給Refit,后者根據(jù)用戶的需求對結(jié)果進(jìn)行解析的過程。
技術(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>
/// 過期時間
/// </summary>
public DateTime ExpiredTime { get; init; }
}服務(wù)端實現(xiàn)
JwtToken.Server 提供兩個后臺服務(wù):一個是登錄驗證服務(wù),為客戶端頒發(fā)用戶憑證(JWT),另一個是獲取系統(tǒng)時間服務(wù)。
在 Program 啟動類,我們需要添加和使用指定服務(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() 兩個方法,代碼如下:
[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>
/// 獲取當(dāng)前時間
/// </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令牌和過期時間。下面是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) // 手機號碼
};
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行代碼:設(shè)置Token的過期時間,這里我們把有效期設(shè)為20分鐘。
客戶端實現(xiàn)
JwtToken.Client 定義后臺服務(wù)調(diào)用接口和實現(xiàn)Token自動續(xù)期。IDemoApi 接口定義如下:
[Headers(new[] { "Authorization:Bearer" })]
public interface IDemoApi
{
/// <summary>
/// 獲取當(dāng)前時間
/// </summary>
/// <returns></returns>
[Get("/Demo/CurrentTime")]
Task<DateTimeOffset> GetCurrentTimeAsync();
}第1行代碼:給 IDemApi 接口加上 [Headers(...)] 特性,這樣每次調(diào)用 GetCurrentTimeAsync() 方法,Http請求頭部都會攜帶此信息。JWT的標(biāo)準(zhǔn)頭部格式為:Authorization: Bearer <token>。
接下來,就是實現(xiàn)Token自動續(xù)期功能。筆者封裝了一個 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 對象的一個委托屬性,用來提供授權(quán)頭部信息,即JWT字符串。
第22至35行代碼:即按照筆者前面的思路轉(zhuǎn)換成代碼,這里就不多介紹了。
最后,我們通過下面一行代碼來獲取后臺系統(tǒng)時間:
var dt = await RestHelper.For<IDemoApi>().GetCurrentTimeAsync();
界面運行效果如下(親測有效):

到此這篇關(guān)于C#利用Refit實現(xiàn)JWT自動續(xù)期詳解的文章就介紹到這了,更多相關(guān)C# Refit實現(xiàn)JWT自動續(xù)期內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#文件復(fù)制異常:"未能找到文件"的解決方案與預(yù)防措施
在C#開發(fā)中,文件操作是基礎(chǔ)中的基礎(chǔ),但有時最基礎(chǔ)的File.Copy()方法也會拋出令人困惑的異常,當(dāng)targetFilePath設(shè)置為D:\25Q1\MR3.6.6.1_C1.2.37_PB250623\bin\gc_data時,系統(tǒng)拋出"未能找到文件"的異常,本文將深入分析這個問題的原因,并提供全面的解決方案2025-08-08
C# Char結(jié)構(gòu)中IsLetterOrDigit(Char)的方法詳解
這篇文章給大家介紹了C#的Char 結(jié)構(gòu)的IsLetterOrDigit(Char)的方法,并通過代碼示例給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-02-02
Unity?UGUI的GraphicRaycaster射線投射組件介紹使用
這篇文章主要為大家介紹了Unity?UGUI的GraphicRaycaster射線投射組件介紹使用,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07

