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

 更新时间:2023年08月18日 11:34:25   作者:天行健君子以自强  
Refit 是一个受到Square的Retrofit库(Java)启发的自动类型安全REST库,这篇文章主要为大家介绍了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 定义如下:

public record TokenResult
    {
        /// <summary>
        /// 访问令牌
        /// </summary>
        public string AccessToken { get; init; }
        /// <summary>
        /// 过期时间
        /// </summary>
        public DateTime ExpiredTime { get; init; }
    }

服务端实现

JwtToken.Server 提供两个后台服务:一个是登录验证服务,为客户端颁发用户凭证(JWT),另一个是获取系统时间服务。

在 Program 启动类,我们需要添加和使用指定服务,从而开启JWT认证和授权。 代码如下:

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>
        /// 获取当前时间
        /// </summary>
        /// <returns></returns>
        [Authorize]
        [HttpGet("CurrentTime")]
        public ValueTask<DateTimeOffset> GetCurrentTimeAsync()
        {
            return ValueTask.FromResult(DateTimeOffset.Now);
        }
    }

第26行代码:给 GetCurrentTimeAsync() 加上 [Authorize] 特性后, 当前服务必须授权后才能访问。

第16行代码:根据用户的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行代码:设置Token的过期时间,这里我们把有效期设为20分钟。

客户端实现

 JwtToken.Client 定义后台服务调用接口和实现Token自动续期。IDemoApi 接口定义如下:

[Headers(new[] { "Authorization:Bearer" })]
    public interface IDemoApi
    {
        /// <summary>
        /// 获取当前时间
        /// </summary>
        /// <returns></returns>
        [Get("/Demo/CurrentTime")]
        Task<DateTimeOffset> GetCurrentTimeAsync();
    }

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

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

/// <summary>
    /// Rest请求服务
    /// </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 对象的一个委托属性,用来提供授权头部信息,即JWT字符串。

第22至35行代码:即按照笔者前面的思路转换成代码,这里就不多介绍了。

最后,我们通过下面一行代码来获取后台系统时间:

var dt = await RestHelper.For<IDemoApi>().GetCurrentTimeAsync();  

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

到此这篇关于C#利用Refit实现JWT自动续期详解的文章就介绍到这了,更多相关C# Refit实现JWT自动续期内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C#开发答题赢钱游戏(自动答题器)

    C#开发答题赢钱游戏(自动答题器)

    现在最火的直播游戏,那就是答题赢钱直播了,如百万英雄、芝士超人、花椒直播、冲顶大会等等,这些游戏的玩法都很简单,答对12题即可瓜分奖金了。玩法虽简单但是完全答对12题难度就挺高了,下面小编给大家带来了C#开发答题赢钱游戏,需要的朋友参考下吧
    2018-01-01
  • C#导出GridView数据到Excel文件类实例

    C#导出GridView数据到Excel文件类实例

    这篇文章主要介绍了C#导出GridView数据到Excel文件类,实例分析了C#使用GridView及Excel的技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-03-03
  • C#调用百度API实现活体检测的方法

    C#调用百度API实现活体检测的方法

    这篇文章主要给大家介绍了关于C#调用百度API实现活体检测的方法,文中通过示例代码介绍的非常详细,对大家学习或者使用C#具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-09-09
  • C#的SQL操作类实例

    C#的SQL操作类实例

    这篇文章主要介绍了C#的SQL操作类实例,涉及到针对数据库的常用操作,在进行C#数据库程序设计中非常具有实用价值,需要的朋友可以参考下
    2014-10-10
  • C#使用Spire.PDF for .NET实现PDF文档打印功能

    C#使用Spire.PDF for .NET实现PDF文档打印功能

    本文将以Spire.PDF for .NET为核心,为大家详细介绍一下如何构建稳定高效的C# PDF打印解决方案,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下
    2025-08-08
  • C#中Dapper的使用教程

    C#中Dapper的使用教程

    Dapper是一款轻量级ORM工具(Github),Dapper语法十分简单。并且无须迁就数据库的设计,今天通过本文给大家介绍C# Dapper的使用,感兴趣的朋友一起看看吧
    2021-07-07
  • C#实现Windows服务安装卸载开启停止

    C#实现Windows服务安装卸载开启停止

    本文主要介绍了C#实现Windows服务安装卸载开启停止,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-07-07
  • Unity3D游戏开发数据持久化PlayerPrefs的用法详解

    Unity3D游戏开发数据持久化PlayerPrefs的用法详解

    在本篇文章里小编给大家整理了关于Unity3D游戏开发之数据持久化PlayerPrefs的使用的相关知识点内容,需要的朋友们参考下。
    2019-08-08
  • C#字符串常见操作总结详解

    C#字符串常见操作总结详解

    本篇文章是对C#中字符串的常见操作进行了详细的总结介绍,需要的朋友参考下
    2013-05-05
  • C# 如何获取处于运行中的Excel、Word对象

    C# 如何获取处于运行中的Excel、Word对象

    这篇文章主要介绍了C# 获取处于运行中的Excel、Word对象操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-01-01

最新评论