• C#利用Refit实现JWT自动续期


    前言

    笔者之前开发过一套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         /// 
     4         /// 访问令牌
     5         /// 
     6         public string AccessToken { get; init; }
     7 
     8         /// 
     9         /// 过期时间
    10         /// 
    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         /// 
     6         /// 登录
     7         /// 
     8         /// 
     9         /// 
    10         [HttpPost("Login")]
    11         public async ValueTask 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         /// 
    23         /// 获取当前时间
    24         /// 
    25         /// 
    26         [Authorize]
    27         [HttpGet("CurrentTime")]
    28         public ValueTask 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         /// 
     4         /// 生成Token
     5         /// 
     6         /// 
     7         public static ValueTask GenerateAsync(int id, string username, string name, string phoneNumber)
     8         {
     9             var claims = new List()
    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         /// 
     5         /// 获取当前时间
     6         /// 
     7         /// 
     8         [Get("/Demo/CurrentTime")]
     9         Task GetCurrentTimeAsync();
    10     }
    复制代码

    第1行代码:给 IDemApi 接口加上 [Headers(...)] 特性,这样每次调用 GetCurrentTimeAsync() 方法,Http请求头部都会加上此信息。JWT的标准授权头部格式为:Authorization: Bearer

    接下来,就是实现Token自动续期功能。笔者封装了一个 RestHelper 类,核心代码如下:

    复制代码
     1     /// 
     2     /// Rest请求服务
     3     /// 
     4     /// 
     5     /// 
     6     public static T For()
     7     {
     8         var settings = new RefitSettings()
     9         {
    10             AuthorizationHeaderValueGetter = () => GetTokenAsync(),
    11         };
    12 
    13         return RestService.For(BaseUrl, settings);
    14     }
    15 
    16     /// 
    17     /// 获取Token
    18     /// 
    19     /// 
    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(jsonStr);
    35             }
    36         }
    37 
    38         return TokenResult?.AccessToken;
    39     }
    复制代码

    第10行代码:AuthorizationHeaderValueGetter 是 RefitSettings 对象的一个委托属性,用来提供授权头部信息,即JWT字符串。

    第22至35行代码:即按照笔者前面的思路转换成代码实现,这里就不再详细说明了。

    最后,我们用一行代码来获取后台系统时间:

    1   var dt = await RestHelper.For().GetCurrentTimeAsync();  

    界面运行效果如下(~亲测有效~):

    参考资料

    认识JWT - 废物大师兄 - 博客园 (cnblogs.com)

    Refit | The automatic type-safe REST library for Xamarin and .NET (reactiveui.github.io)

  • 相关阅读:
    React-1 基础知识
    vue @cliick.stop @click.prevent @click.self
    自研 MySQL Binlog 分析程序介绍
    【matplotlib基础】--绘图配置
    C# AsyncLocal 是如何实现 Thread 间传值
    【3】IMU模块:PA-IMU-460 ROS驱动 + 与GNSS时间同步
    【Shell编程】字符截取命令cut、printf命令
    提高编程效率专辑—数据导入工具①
    分布式架构-流量治理-流量控制
    【测试人】我再也不当背锅侠,防锅指南......
  • 原文地址:https://www.cnblogs.com/fengjq/p/17631841.html