前言

笔者之前开发过一套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)

C#利用Refit实现JWT自动续期的更多相关文章

  1. .net core使用jwt自动续期

    小弟不C才,最近看了下网上的jwt方案,于是自己写了一个简单的jwt方案和大家分享下,希望大家给点意见! 假如有一个读书网站,可以不用登陆就访问,当需要自己写文章的时候就必须登录,并且登录之后如果一段 ...

  2. 关于Token和Cookie做权限校验的区别及Token自动续期方案

    title: 关于Token和Cookie做权限校验的区别及Token自动续期方案 categories: 后端 tags: - .NET Token和Cookie的区别 首先,要知道一些基本概念:h ...

  3. 利用phantomjs模拟QQ自动登录

    之前为了抓取兴趣部落里的数据,研究了下QQ自动登录. 当时搜索了一番,发现大部分方法都已经失效了,于是准备自己开搞. 第一个想到的就是参考网上已有方案的做法,梳理登陆js的实现,通过其他语言重写.考虑 ...

  4. [DevExpress]利用LookUpEdit实现类似自动提示效果

    原文:[DevExpress]利用LookUpEdit实现类似自动提示效果 关键代码: public static void BindWithAutoCompletion(this LookUpEdi ...

  5. Windows 2003】利用域&&组策略自动部署软件

    Windows 2003]利用域&&组策略自动部署软件 转自 http://hi.baidu.com/qu6zhi/item/4c0fa100dc768613cc34ead0 ==== ...

  6. [转]部署Let’s Encrypt免费SSL证书&&自动续期

    最近公司网站要用https,从自己摸索到找到国内的免费证书到选购正式的收费证书,最后老板说:太贵!不要.一脸懵逼的听老板提到Let's Encrypt证书,没办法,用呗.之前是有一些了解,国外发布的一 ...

  7. 让网站永久拥有HTTPS - 申请免费SSL证书并自动续期 Let’s Encrypt

    让网站永久拥有HTTPS - 申请免费SSL证书并自动续期 Let’s Encrypt 为什么要用HTTPS  网站没有使用HTTPS的时候,浏览器一般会报不安全,而且在别人访问这个网站的时候,很有可 ...

  8. 怎样查看SSL证书的有效期?自动续期是否生效?

    前面一篇教程教大家如何能够把网站的 HTTPS 的 SSL 证书自动续期.料神米课的学员动手能力都很强,已经很多都成功把证书续期了.但怎么看证书续期是否成功了呢? 使用火狐 firefox 浏览器就可 ...

  9. 利用AutoSPSourceBuilder和Autospinstaller自动安装SharePoint Server 2013图解教程——Part 1

    这是一篇对之前 <利用AutoSPSourceBuilder和Autospinstaller自动安装SharePoint Server 2013图解教程——Part 2>的补充.本篇博客将 ...

  10. 使用acme.sh申请&自动续期LetsEncrypt免费SSL证书(转)

    一.简介 LetsEncrypt是一个免费.自动.开放的证书颁发机构.acme.sh 实现了 acme 协议, 可以从 LetsEncrypt 生成免费的证书. 本文介绍如何使用acme.sh来签发并 ...

随机推荐

  1. 2021-07-09:股票问题6。给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。你可以无限次地完成交易,但是你每笔交易都需要付

    2021-07-09:股票问题6.给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 :整数 fee 代表了交易股票的手续费用.你可以无限次地完成交易,但是你每笔交易都需要付 ...

  2. Docker运行Django框架

    Django框架 创建django-pg项目目录 [root@docker ~]# mkdir docker-compose-django [root@docker ~]# cd docker-com ...

  3. 浅谈 ByteHouse Projection 优化实践

    预聚合是 OLAP 系统中常用的一种优化手段,在通过在加载数据时就进行部分聚合计算,生成聚合后的中间表或视图,从而在查询时直接使用这些预先计算好的聚合结果,提高查询性能,实现这种预聚合方法大多都使用物 ...

  4. 「学习笔记」DP 学习笔记1

    序列 DP 一般序列 DP 核心思想:将序列的前 \(i\) 个数的状态用一个更简单的形式表示出,并且体现出这些状态对后续的影响. 题目 ABC 267D 给定一个序列 \(a\),找到一个长度为 \ ...

  5. [ESP] 私有版Rainmaker User Mapping

    [ESP] 私有版Rainmaker User Mapping 1. 设备烧录的程序esp-rainmaker/examples/gpio这个demo 我这里是自己的工程,可以参照 idf.py se ...

  6. 把jar包打成docker镜像并推送到Docker Hub

    1.准备需要的jar包并复制到服务器某个目录下 2.在此目录下,创建Dockerfile的文本文件,并将以下内容添加到文件中: # 基础镜像 FROM openjdk:8-jre # author(可 ...

  7. 体细胞突变检测分析流程-系列1( WES&Panel)

    Sentieon●体细胞变异检测-系列1   Sentieon 致力于解决生物信息数据分析中的速度与准确度瓶颈,通过算法的深度优化和企业级的软件工程,大幅度提升NGS数据处理的效率.准确度和可靠性. ...

  8. 脱发秘籍:前端Chrome调试技巧汇总

    Chrome浏览器调试工具的核心功能: 注:本文测试.截图均为Edge浏览器(内核是Chromium),浏览器内核可了解<有哪些浏览器/内核?> 00.基础操作汇总 操作类型 快捷键/说明 ...

  9. S32Kxxx bootloader之LIN bootloader

    了解更多关于bootloader 的C语言实现,请加我Q扣: 1273623966 (验证信息请填 bootloader),欢迎咨询或定制bootloader(在线升级程序). 上一次发布博文到如今既 ...

  10. Java面试题全集(一)

    JDK.JRE.JVM之间的区别 JDK(Java SE Development Kit),Java标准开发包,它提供了编译.运⾏Java程序所需的各种⼯具和资源,包括Java编译器.Java运⾏时环 ...