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,一经查实,立即删除!

相关文章

C和指针之字符串简单实现strchr、strcmp函数

1、问题 简单实现strchr、strcmp函数 2、代码实现 #include <stdio.h> #include <string.h>/** 简单模拟strchr函数 **/ char *my_strchr(const char *des, int ch) {if (des == NULL)return des;while (*des != \0){if (*des == ch){return des; }++des;}return …

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

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

redis总结笔记

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

WinHEC(Windows硬件project产业创新峰会)将2015回归

WinHEC这是Windows Hardware Engineering Cumminity&#xff0c;中国呼吁Windows硬件project产业创新峰会。将2015在早期的回报&#xff0c;2015年3月18日至19日在中国深圳召开–微软决定在深圳召开WinHEC也是考虑到深圳的硬件生态。拥有大量硬件公司&#xff0c;包含电子设计、…

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;是目前最流行的基于 …

C和指针之字符串总结

1、C语言字符串分为字符串常量、字符数组&#xff0c;字符串常量适用于程序对它不被修改&#xff0c;需要修改字符串一般用字符数组或者是一个指向动态分配内存的数组指针。 2、需要注使用strcpy和strcat函数的时候需要保证目标函数的内存空间足够 3、使用strcmp函数的时候&…

linux shell 中文件编码查看及转换方法

一、查看文件编码。 在打开文件的时候输入:set fileencoding 即可显示文件编码格式。 二、文件编码转换 1、在Vim中直接进行转换文件编码,比如将一个文件转换成utf-8格式 在打开的文件中输入:set fileencodingutf-8 2、使用iconv转换&#xff0c;iconv…

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厘米的缝份车…

C和指针之结构体大小和成员变量位置距离结构开始存储的位置偏移字节

1、问题 1)、结构体大小 结构体成员的内存分配满足下面三个条件 2 结构体第一个成员的地址和结构体的首地址相同 3 结构体每个成员地址相对于结构体首地址的偏移量是该成员大小的整数倍,如果不是则编译器会在成员之间添加填充字节 4 结构体总的大小要是其成员中最大si…

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…

算法笔记_226:填符号凑算式(Java)

目录 1 问题描述 2 解决方案 1 问题描述 匪警请拨110,即使手机欠费也可拨通&#xff01; 为了保障社会秩序&#xff0c;保护人民群众生命财产安全&#xff0c;警察叔叔需要与罪犯斗智斗勇&#xff0c;因而需要经常性地进行体力训练和智力训练&#xff01; 某批警察叔叔正在进行…

design短语的用法总结_design的意思、用法、搭配和例句

designvi. vt.设计&#xff1b;计划&#xff1b;构思&#xff1b;n.设计&#xff1b;图案&#xff1b;构思design sth. to do打算做……be designed to do目的是……be designed for sb. / sth.打算给(做)……用be designed as sth.打算当作……He designed us a poster.他为我…

WPF初学——自定义样式

在WPF开发界面的过程中&#xff0c;经常会遇到多个相同种类的控件样式也是统一的&#xff0c;但是要一个一个地去设置&#xff0c;即使是复制粘贴都嫌累&#xff0c;所以翻了些书&#xff0c;网上度娘了一些材料&#xff0c;发现WPF有很容易的方式解决我的纠结&#xff0c;那就…

Xamarin效果第二篇之公众号App

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

C和指针之联合体大小

1、联合体的特点和大小 union是共用一个内存首地址,联合体中每个成员的地址都相同,等于联合体变量的首地址 联合体的大小足够容纳最宽的成员,大小能被其包含的所有基本数据类型的大小所整除 2、测试Demo #include <stdio.h>union var {long int a;int b; };union size…

新的斐波那契数列

转载请标明出处&#xff1a;牟尼的专栏 http://blog.csdn.net/u012027907 Problem1&#xff1a; 题目描写叙述&#xff1a; 定义一个新的斐波那契数列&#xff1a; F(0)7。 F(1)11; F(n)F(n-1)F(n-2);(n>2) 输入&#xff1a; 输入有多组&#xff1b;首先输入一个N&…