Jwt Token 的刷新机制设计

Jwt Token 的刷新机制设计

Intro

前面的文章我们介绍了如何实现一个简单的 Jwt Server,可以实现一个简单 Jwt 服务,但是使用 Jwt token 会有一个缺点就是 token 一旦颁发就不能够进行作废,所以通常 jwt token 的有效期一般会比较短,但是太短了又会比较影响用户的用户体验,所以就有了 refresh token 的参与,一般来说 refresh token 会比实际用的 access token 有效期会长一些,当 access token 失效了,就使用 refresh token 重新获取一个 access token,再使用新的 access_token 来访问服务。

Sample

我们的示例在前面的基础上增加了 refresh_token,使用示例如下:

注册服务的时候启用 refresh_token 就可以了

services.AddJwtTokenService(options =>
{options.SecretKey = Guid.NewGuid().ToString();options.Issuer = "https://id.weihanli.xyz";options.Audience = "SparkTodo";// EnableRefreshToken, disabled by defaultoptions.EnableRefreshToken = true;
});

启用了 refresh token 之后,在生成 token 的时候就会返回一个带着 refresh token 的 token 对象(TokenEntityWithRefreshToken) 否则就是返回只有 acess token 的对象 (TokenEntity)

public class TokenEntity
{public string AccessToken { get; set; }public int ExpiresIn { get; set; }
}public class TokenEntityWithRefreshToken : TokenEntity
{public string RefreshToken { get; set; }
}

然后我们就可以使用 refresh token 来获取新的 access token 了,使用方式如下:

[HttpGet("RefreshToken")]
public async Task<IActionResult> RefreshToken(string refreshToken, [FromServices] ITokenService tokenService)
{return await tokenService.RefreshToken(refreshToken).ContinueWith(r =>r.Result.WrapResult().GetRestResult());
}

GetToken 接口和上次的示例相比稍微有一些改动,主要是体现了有没有 refresh token 的差异,ValidateToken 和之前一致

[HttpGet("getToken")]
public async Task<IActionResult> GetToken([Required] string userName, [FromServices] ITokenService tokenService)
{var token = await tokenService.GenerateToken(new Claim("name", userName));if (token is TokenEntityWithRefreshToken tokenEntityWithRefreshToken){return tokenEntityWithRefreshToken.WrapResult().GetRestResult();}return token.WrapResult().GetRestResult();
}[HttpGet("validateToken")]
public async Task<IActionResult> ValidateToken(string token, [FromServices] ITokenService tokenService)
{return await tokenService.ValidateToken(token).ContinueWith(r =>r.Result.WrapResult().GetRestResult());
}

验证步骤如下:

  • 获取 token

5581a32efc25f129baa85a5e6f1c7076.png

4867fb7721b653f3f63cc50fae76a660.png

access token

754a276d9342a300afb8515e0a985544.pngrefresh token


  • 验证 access token

d8826fc044b9703fbe95996127a2b900.png

  • 使用 refresh token 验证 token

c0e258fbdcb526f5b42a6bb6d2115ecd.png

  • 使用 refresh token 获取新的 access token

64f248e539d4a5bf7a7180ca83307c0c.png

renew token with the refresh token

2f43db6a4433ca76d33187fb1d8b077f.png

new access token
  • 验证新的 access token

88b9f7851b141ed19e9e82ed83fc9f8a.png

validate token with the new access token

Implement

从上面 token 解析出来的内容大概可以看的出来实现的思路,我的实现思路是仍然使用 Jwt 这套机制来生成和验证 refresh token,只是 refresh token 的 audience 和 access token 不同,另外 refresh token 的有效期一般会更长一些,这样我们就不能把 refresh token 直接当作 access token 来使用,因为 token 验证会失败,而之所以利用 Jwt 的机制来实现也是希望能够简化 refresh token,利用 jwt 的无状态,不需要使得无状态的应用变得有状态,有看过一些别的实现是直接使用存储将 refresh token 保存起来,这样 refresh token 就变成有状态的了,还要依赖一个存储,当然如果你希望使用有状态的 refresh token 也是可以自己扩展的,下面来看一些实现代码

ITokenService 提供了 token 服务的抽象,定义如下:

public interface ITokenService
{Task<TokenEntity> GenerateToken(params Claim[] claims);Task<TokenValidationResult> ValidateToken(string token);Task<TokenEntity> RefreshToken(string refreshToken);
}

JwtTokenService 是基于 Jwt 的 Token 服务实现:

public class JwtTokenService : ITokenService
{private readonly JwtSecurityTokenHandler _tokenHandler = new();private readonly JwtTokenOptions _tokenOptions;private readonly Lazy<TokenValidationParameters>_lazyTokenValidationParameters,_lazyRefreshTokenValidationParameters;public JwtTokenService(IOptions<JwtTokenOptions> tokenOptions){_tokenOptions = tokenOptions.Value;_lazyTokenValidationParameters = new(() =>_tokenOptions.GetTokenValidationParameters());_lazyRefreshTokenValidationParameters = new(() =>_tokenOptions.GetTokenValidationParameters(parameters =>{parameters.ValidAudience = GetRefreshTokenAudience();}));}public virtual Task<TokenEntity> GenerateToken(params Claim[] claims)=> GenerateTokenInternal(_tokenOptions.EnableRefreshToken, claims);public virtual Task<TokenValidationResult> ValidateToken(string token){return _tokenHandler.ValidateTokenAsync(token, _lazyTokenValidationParameters.Value);}public virtual async Task<TokenEntity> RefreshToken(string refreshToken){var refreshTokenValidateResult = await _tokenHandler.ValidateTokenAsync(refreshToken, _lazyRefreshTokenValidationParameters.Value);if (!refreshTokenValidateResult.IsValid){throw new InvalidOperationException("Invalid RefreshToken", refreshTokenValidateResult.Exception);}return await GenerateTokenInternal(false,refreshTokenValidateResult.Claims.Where(x => x.Key != JwtRegisteredClaimNames.Jti).Select(c => new Claim(c.Key, c.Value.ToString() ?? string.Empty)).ToArray());}protected virtual Task<string> GetRefreshToken(Claim[] claims, string jti){var claimList = new List<Claim>((claims ?? Array.Empty<Claim>()).Where(c => c.Type != _tokenOptions.RefreshTokenOwnerClaimType).Union(new[] { new Claim(_tokenOptions.RefreshTokenOwnerClaimType, jti) }));claimList.RemoveAll(c =>JwtInternalClaimTypes.Contains(c.Type)|| c.Type == JwtRegisteredClaimNames.Jti);var jtiNew = _tokenOptions.JtiGenerator?.Invoke() ?? GuidIdGenerator.Instance.NewId();claimList.Add(new(JwtRegisteredClaimNames.Jti, jtiNew));var now = DateTimeOffset.UtcNow;claimList.Add(new Claim(JwtRegisteredClaimNames.Iat, now.ToUnixTimeMilliseconds().ToString(), ClaimValueTypes.Integer64));var jwt = new JwtSecurityToken(issuer: _tokenOptions.Issuer,audience: GetRefreshTokenAudience(),claims: claimList,notBefore: now.UtcDateTime,expires: now.Add(_tokenOptions.RefreshTokenValidFor).UtcDateTime,signingCredentials: _tokenOptions.SigningCredentials);var encodedJwt = _tokenHandler.WriteToken(jwt);return encodedJwt.WrapTask();}private static readonly HashSet<string> JwtInternalClaimTypes = new(){"iss","exp","aud","nbf","iat"};private async Task<TokenEntity> GenerateTokenInternal(bool refreshToken, Claim[] claims){var now = DateTimeOffset.UtcNow;var claimList = new List<Claim>(){new (JwtRegisteredClaimNames.Iat, now.ToUnixTimeMilliseconds().ToString(), ClaimValueTypes.Integer64)};if (claims != null){claimList.AddRange(claims.Where(x => !JwtInternalClaimTypes.Contains(x.Type)));}var jti = claimList.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Jti)?.Value;if (jti.IsNullOrEmpty()){jti = _tokenOptions.JtiGenerator?.Invoke() ?? GuidIdGenerator.Instance.NewId();claimList.Add(new(JwtRegisteredClaimNames.Jti, jti));}var jwt = new JwtSecurityToken(issuer: _tokenOptions.Issuer,audience: _tokenOptions.Audience,claims: claimList,notBefore: now.UtcDateTime,expires: now.Add(_tokenOptions.ValidFor).UtcDateTime,signingCredentials: _tokenOptions.SigningCredentials);var encodedJwt = _tokenHandler.WriteToken(jwt);var response = refreshToken ? new TokenEntityWithRefreshToken(){AccessToken = encodedJwt,ExpiresIn = (int)_tokenOptions.ValidFor.TotalSeconds,RefreshToken = await GetRefreshToken(claims, jti)} : new TokenEntity(){AccessToken = encodedJwt,ExpiresIn = (int)_tokenOptions.ValidFor.TotalSeconds};return response;}private string GetRefreshTokenAudience() => $"{_tokenOptions.Audience}_RefreshToken";
}

在生成 refresh token 的时候会把关联的 access token 的 jti(jwt token 的 id,默认是一个 guid 可以通过option 自定义)写到 access token 中,claim type 可以通过 option 自定义,这样如果想要实现 refresh token 所属的 access token 的匹配校验也是可以实现的。

生成 refresh token 的时候会把生成 access token 时的 claims 信息也会生成在 refresh token 中,这样做的好处在于使用 refresh token 刷新 access token 的时候就可以直接根据 refresh token 生成 access token 无需别的信息,刷新得到的 access-token 中会有之前的 access token 的一个 id,如果想要记录所有 token 的颁发过程也是可以实现的。

如果想要实现有状态的 Refresh token 只需要重写 JwtTokenServiceGetRefreshTokenRefreshToken 两个虚方法即可

Integration with JwtBearerAuth

如何和 asp.net core 的 JwtBearerAuthentication 进行集成呢?为了方便集成,提供了一个扩展来方便的集成,只需要使用 AddJwtTokenServiceWithJwtBearerAuth 来注册即可,实现代码如下:

public static IServiceCollection AddJwtTokenServiceWithJwtBearerAuth(this IServiceCollection serviceCollection, Action<JwtTokenOptions> optionsAction, Action<JwtBearerOptions> jwtBearerOptionsSetup = null)
{Guard.NotNull(serviceCollection);Guard.NotNull(optionsAction);if (jwtBearerOptionsSetup is not null){serviceCollection.Configure(jwtBearerOptionsSetup);}serviceCollection.ConfigureOptions<JwtBearerOptionsPostSetup>();return serviceCollection.AddJwtTokenService(optionsAction);
}

JwtBearerOptionsPostSetup 实现如下:

internal sealed class JwtBearerOptionsPostSetup :IPostConfigureOptions<JwtBearerOptions>
{private readonly IOptions<JwtTokenOptions> _options;public JwtBearerOptionsPostSetup(IOptions<JwtTokenOptions> options){_options = options;}public void PostConfigure(string name, JwtBearerOptions options){options.Audience = _options.Value.Audience;options.ClaimsIssuer = _options.Value.Issuer;options.TokenValidationParameters = _options.Value.GetTokenValidationParameters();}
}

JwtBearerOptionsPostSetup 主要就是配置的 JwtBearerOptionsTokenValidationParameters 以使用配置好的一些参数来进行验证,避免了两个地方都要配置

使用示例如下:

首先我们准备一个 API 来验证 Auth 是否成功,API 很简单,定义如下:

[HttpGet("[action]")]
[Authorize(AuthenticationSchemes = "Bearer")]
public IActionResult BearerAuthTest()
{return Ok();
}

我们先获取一个 access token,然后调用接口来验证 Auth 能否成功

1695b9f3412b0b26676b8987506c1795.png

Bearer token test

9adc5655f8559e03e28629440fe61d45.png

No token

More

除了上面的示例,你也可以参考这个项目 https://github.com/WeihanLi/SparkTodo/tree/master/SparkTodo.API,之前独立使用 Jwt token 的,现在也使用了上面的实现

目前的实现基于可以满足我自己的需要了,还有一些可以优化的点

  • 现在对于 refresh token 的校验可以优化一下,目前只是验证了一个 refresh token 的合法性,验证 owner jwt token id 虽然可以实现,但是有些不太方便,可以优化一下

  • 现在 refresh token 签名用到的 key 和 access token 是同一个,应该允许用户分开配置

  • 使用 refresh token 获取新的 token 时只返回 access token,可以支持返回新的 token 时返回 refresh_token

你觉得还有哪些需要改进的地方呢?

References

  • https://github.com/WeihanLi/SparkTodo

  • https://github.com/WeihanLi/SparkTodo/tree/master/SparkTodo.API

  • https://github.com/WeihanLi/WeihanLi.Web.Extensions

  • https://github.com/WeihanLi/WeihanLi.Web.Extensions/tree/dev/samples/WeihanLi.Web.Extensions.Samples

  • 更轻易地实现 Jwt Token

8a55c07cf9f9fe6c4a75548e90252eae.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/290249.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

辨别真假数据科学家必备手册:深度学习45个基础问题(附答案)

简介 早在2009年&#xff0c;深度学习还只是一个新兴领域&#xff0c;只有少数人认为它是一个多产的研究方向。今天&#xff0c;深度学习正在被用来开发那些过去被认为是不可能完成的应用。 语音识别&#xff0c;图像识别&#xff0c;数据中的模式识别&#xff0c;照片中的对象…

redis总结笔记

为什么80%的码农都做不了架构师&#xff1f;>>> 1、Redis的介绍和安装部署 NOSQL 》 Not Only SQL NOSQL以key-value形式存储 特点:非关系型、分布式、开源的、水平可扩展 NOSQL: 数据高并发读写 对海量数据的高效率存储和访问 对数据的搞可扩展性和高可用性 Redi…

go kegg_GO,KEGG富集分析工具——DAVID

DAVID(https://david.ncifcrf.gov/home.jsp)是一个生物信息数据库&#xff0c;整合了生物学数据和分析工具&#xff0c;为大规模的基因或蛋白列表(成百上千个基因ID或者蛋白ID列表)提供系统综合的生物功能注释信息&#xff0c;帮助用户从中提取生物学信息。DAVID目前的工具可以…

更轻易地实现 Jwt Token

更轻易地实现一个 Jwt ServerIntro最近在多个项目中都有用到 Jwt Token 认证&#xff0c;就想着把之前项目里 Jwt Token 的使用封装一下&#xff0c;以便于之后集成起来更加地方便&#xff0c;不用再拷贝代码了JWTJWT 是 JSON Web Token 的缩写&#xff0c;是目前最流行的基于 …

android之实现各个组件点击事件处理

android之实现各个组件点击事件处理&#xff1a;注意&#xff1a;&#xff08;TextView这个组件要点击产生效果的话&#xff0c;要设置&#xff0c;android:clickable"true"这个属性&#xff09;布局&#xff1a;layout/activity_main.xml<LinearLayout xmlns:and…

Android开发最佳实践《IT蓝豹》

Android开发最佳实践 移动开发Android经验分享应用GoogleMaterial Design摘要&#xff1a;前 段时间&#xff0c;Google公布了Android开发最佳实践的一系列课程&#xff0c;涉及到一些平时开发过程中应该保持的良好习惯以及如何使用最新的Android Design Support Library来快速…

.NET MAUI 已在塔架就位 ,4月份发布RC

最美人间三月天&#xff0c;春光不负赶路人。在充满无限希望的明媚春天里&#xff0c;一路风雨兼程的.NET 团队正奋力实现新的突破。根据计划&#xff0c;新一代移动开发平台MAUI 将于4月份 发布RC。目前&#xff0c;MAUI的测试工作和火箭发射前各项准备工作在github 上按计划有…

如何把照片正面变成反面_没有锁边机如何做衣服(五种方法)

这么多年一直没有锁边机&#xff0c;但是也做了很多衣服&#xff0c;今天给大家分享一些我曾经用过的方法。来去缝来去缝适合缝制轻薄面料&#xff0c;如雪纺、真丝、欧根纱等。反反相对&#xff0c;缝份0.5厘米把缝份剪掉0.2厘米翻过来使正面相对&#xff0c;留0.5厘米的缝份车…

linux线程池资料

2019独角兽企业重金招聘Python工程师标准>>> http://www.360doc.com/content/13/0728/13/13308646_303116654.shtml http://blog.csdn.net/turkeyzhou/article/details/8755976 http://blog.csdn.net/zhoubl668/article/details/8927090 http://blog.csdn.net/zypue…

Xamarin效果第二篇之公众号App

前面简单摸索一下Xamarin然后简单做了一个时间轴;这不这几天再次基于Xamarin实现了一下公众号App;我也就是瞎折腾,闲话不多扯,上效果:主Page直接用TabbedPage(类似WPF中的TabControl)然后后台添加内容Page:”互动“页使用CollectionView和模板选择器&#xff1a;"发表&quo…

K8S原来如此简单(一)K8S核心组件与基本原理

k8s视频课程K8S核心组件与工作原理k8s官方文档&#xff1a;https://kubernetes.io/zh/docs/home/前提掌握容器技术&#xff1a;Docker&#xff0c;Containerd等K8S优势使用简单&#xff0c;少量人/小团队可以轻松维护大型分布式系统全面拥抱微服务架构&#xff0c;快速迭代&…

docker supervisor管理进程

Supervisor管理进程Docker容器在启动的时候开启单个进程&#xff0c;比如&#xff0c;一个ssh或者apache的daemon服务。但我们经常需要在一个机器上开启多个服务&#xff0c;这可以有很多方法&#xff0c;最简单的就是把多个启动命名放到一个启动脚本里面&#xff0c;启动的时候…

理解Linux系统中的load average

一、什么是load average&#xff1f;linux系统中的Load对当前CPU工作量的度量 (WikiPedia: the system load is a measure of the amount of work that a computer system is doing)。也有简单的说是进程队列的长度。Load Average 就是一段时间 (1 分钟、5分钟、15分钟) 内平均…

【贯穿】.NET6结合Docker傻瓜式实现容器编排

常规开发部署的痛点一个项目的开发上线有很多纷繁复杂的问题&#xff0c;例如&#xff1a;操作系统运行环境以及各种应用配置、集群环境搭建等等。特别是各种版本的迭代导致的不兼容&#xff0c;这些对于曾经的架构师而言也十分苦恼。而Docker的出现实现了从“蚂蚁搬家”到“乾…

bootstrapt 表格自适应_一起聊B端设计 - 如何设计表格?

一、 数据查看让我们先来回顾一下表格的基本构成&#xff0c;最上面的为表头&#xff0c;横为行&#xff0c;纵为列&#xff0c;内容区每一组展示数据区域为单元格。 表格的设计&#xff0c;虽然看似简单&#xff0c;但是作为用户最常用的组件之一&#xff0c;我们需要对视觉和…

.NET 6 攻略大全(二)

点击上方蓝字关注我们&#xff08;本文阅读时间&#xff1a;15分钟)接上篇内容&#xff0c;本篇文章将介绍&#xff1a;Arm64、容器、支持 OpenTelemetry 指标、Windows Forms 的相关攻略。 Arm64这些天来&#xff0c;对于笔记本电脑、云硬件和其他设备来说&#xff0c;Arm64 令…

ubuntu 开启 apache mod_rewrite

2019独角兽企业重金招聘Python工程师标准>>> ci里需要隐藏index.php的输入需要使用apache的rewrite模块,按照下面的步骤开启mod_rewrite http://www.dev-metal.com/enable-mod_rewrite-ubuntu-14-04-lts/ 转载于:https://my.oschina.net/u/1177171/blog/354202

composer切换源_Composer具体安装方法

composer 作为依赖管理工具&#xff0c;使用频率还是挺高的。特别是对于我这种比较懒的程序猿&#xff0c;有现成轮子的时候坚决不自己重复造轮子。它主要有三部分构成&#xff1a;命令行工具&#xff0c;包仓库&#xff0c;代码库。包仓库就是我们常说的 composer 源&#xff…

C# 操作FireBird 附源码

写了一个C#操作firebird数据库的小Demo&#xff0c;有需要的可以研究研究, 步骤&#xff1a; 1.创建数据库 2.建数据表&#xff0c;插入数据&#xff0c;并读取、 写的时候碰到N多奇葩问题&#xff0c;记录了一些 解决方案&#xff1a; 程序集-生成-目标平台 改成 x86 源码地址…

.NET 6 攻略大全(三)

点击上方蓝字关注我们&#xff08;本文阅读时间&#xff1a;15分钟).NET 6 继续与大家相约周日啦。本篇文章将介绍&#xff1a;单文件应用、IL 修整、System.Text.Json、源代码构建、库AIP的相关攻略。 单文件应用 在 .NET 6中&#xff0c;已为 Windows 和 macOS 启用内存中单文…