当前位置: 首页 > news >正文

【ASP.NET Core】双Token机制在ASP.NET Core中的实现

文章目录

  • 前言
  • 一、设计思路
  • 二、执行流程
    • 2.1 登录成功生成双Token
    • 2.2 根据refreshToken刷新accessToken
  • 总结


前言

现代前后端分离的模式中,一般都是采用token的方式实现API的鉴权,而不是传统Web应用中依赖服务器端的Session存储和客户端Cookie的自动传递匹配机制。前端发起的请求时,在其请求头内传入“Authorization:token”,后端解析请求头中的token, 获取载荷信息过期时间等状态信息,验证Token是否有效,实现鉴权。

本篇文章聚焦于在ASP.NET Core中实现身份验证中双Token(accessToken + refreshToken)的颁发,来满足前端无感刷新。

前端部分的设计可以参考小程序的这篇文章 链接: 【微信小程序】微信小程序基于双token的API请求封装与无感刷新实现方案


一、设计思路

本文采用双token的方式(accessToken + refreshToken)。accessToken生命周期短,前端作为请求头写入请求传给后端用于鉴权,refreshToken生命周期长,用于刷新accessToken。本方案核心目标是解决accessToken过期后,前端将refreshToken传入后端,后端能通过refreshToken用,返回一个新的accessToken供前端使用,而不是重复登录。

并且还将完善refreshToken泄露导致的安全风险,将accessToken和refreshToken匹配。也就是说执行刷新token的时候,服务端需要同时在请求中获取accessToken和refreshToken。(考虑到安全性refreshToken可以采取RSA加密)

二、执行流程

2.1 登录成功生成双Token

双token中accessToken使用jwt的方案生成,不需要保存在服务器端。前端发起的请求头中携带accessToken,后端根据标准的jwt解析流程鉴权。refreshToken作为一个键值对的键,需要保存到服务器,推荐使用redis。refreshToken这个键是一个Guid,保证其唯一性,并且refreshToken对应的值里需要有一个标识符,用于确定这个refreshToken是否能刷新生成新的accessToken。

本方案采用一个为Guid的SessionUId,将accessToken和refreshToken匹配。

首先是生成双token前,初始化sessionUId。然后生成JWT的时候在载荷里添加sessionUId。再生成refreshToken的时候,也将SessionUId传入实例后的RefreshTokenInfo对象。

accessToken生命周期的,refreshToken生命周期长。在refreshToken生命周期内,它可以刷新其匹配的accessToken。其中refreshToken也可以采取滑动过期策略,每一次刷新accessToken都会延长refreshToken过期时间。

refreshToken作为键对应的值对象

public class RefreshTokenInfo
{/// <summary>/// 当前用户ID/// </summary>public Guid SessionUId { get; set; }/// <summary>/// 当前用户ID/// </summary>public int UserID { get; set; }/// <summary>/// 刷新令牌的创建时间/// </summary>public DateTime CreatedAt { get; set; }
}

生成JWT的时候传入sessionUId

public string GenerateJWT(CurrentUser currentUser,string sessionUId)
{var claims = new List<Claim>() {new Claim(ClaimTypes.Name, currentUser.Name),new Claim("UserId",currentUser.UserId.ToString()),new Claim("SessionUId",sessionUId)};foreach (var roles in currentUser.RoleList){claims.Add(new Claim(ClaimTypes.Role, roles));}//准备加密keySymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.SecurityKey));//HmacSha256加密方式SigningCredentials credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);JwtSecurityToken token = new JwtSecurityToken(issuer: _jwtOptions.Issuer,audience: _jwtOptions.Audience,claims: claims,expires: DateTime.Now.AddSeconds(_jwtOptions.ExpireTime),signingCredentials: credentials);return new JwtSecurityTokenHandler().WriteToken(token);
}

返回双token

/// <summary>
/// 生成双Token
/// </summary>
/// <param name="openId">用户唯一标识</param>
/// <returns>Token响应</returns>
public async Task<TokenResponse> GenerateTokensAsync(CurrentUser currentUser)
{//会话ID,每次登录生成一个,用于将accessToken和refeshToken匹配Guid sessionUId = Guid.NewGuid();string accessToken = _jwtService.GenerateJWT(currentUser, sessionUId.ToString());string refreshToken = GenerateRefreshToken();string refreshTokenExpireTime = _configuration["RefreshTokenOptions:ExpireTime"];if (refreshTokenExpireTime == null || refreshTokenExpireTime == "" ||!int.TryParse(refreshTokenExpireTime, out int expireTime)){throw new ConfigException("未配置刷新Token的过期时间");}RefreshTokenInfo refreshTokenInfo = new RefreshTokenInfo{SessionUId = sessionUId,UserID = currentUser.UserId,CreatedAt = DateTime.UtcNow};await _redisdb.StringSetAsync($"{_redisKeyPrefix}refreshToken:{refreshToken}", JsonSerializer.Serialize(refreshTokenInfo), TimeSpan.FromSeconds(expireTime));return new TokenResponse{AccessToken = accessToken,RefreshToken = refreshToken};
}

2.2 根据refreshToken刷新accessToken

前端发起刷新accessToken的时候需要把accessToken和refreshToken一并带上。其中accessToken还是采用请求头,refreshToken可以作为FromBody传入。

首先我们需要一个分析refreshToken的函数来判断refreshToken是否在redis(内存)中存在,主要是解析并获取到sessionUId。

/// <summary>
/// 分析RefreshToken
/// </summary>
/// <param name="refreshToken"></param>
/// <returns></returns>
/// <exception cref="LoginFailedException"></exception>
public async Task<RefreshTokenInfo> AnalysisRefreshToken(string refreshToken)
{string? tokenValue = await _redisdb.StringGetAsync($"{_redisKeyPrefix}refreshToken:{refreshToken}");if (tokenValue is null || tokenValue == ""){throw new LoginFailedException("登录信息失效,请重新登录");}RefreshTokenInfo refreshTokenInfo = JsonSerializer.Deserialize<RefreshTokenInfo>(tokenValue);if (refreshTokenInfo == null || refreshTokenInfo.SessionUId == Guid.Empty){throw new LoginFailedException("登录信息失效,请重新登录");}return refreshTokenInfo;
}

然后获取HttpContext,请求头中的accessToken

var authHeader = HttpContext.Request.Headers["Authorization"].FirstOrDefault();
if (!(authHeader != null && authHeader.StartsWith("Bearer")))
{throw new ArgumentException("请求获取refeshToken的时候未携带有效的token");
}
string? token = authHeader?.Substring("Bearer ".Length).Trim();

并且解析,获取载荷中的sessionUId

public ClaimsPrincipal ParseTokenClaims(string token)
{var tokenHandler = new JwtSecurityTokenHandler();// 令牌已过期,但我们仍然尝试解析它(只验证签名)var jwtToken = tokenHandler.ReadJwtToken(token);// 验证签名var validationParameters = new TokenValidationParameters{ValidateIssuerSigningKey = true,IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.SecurityKey)),ValidateIssuer = true,ValidateAudience = false,ValidIssuer = _jwtOptions.Issuer,ValidAudience = _jwtOptions.Audience,ValidateLifetime = false // 不验证有效期};try{// 只验证签名和发行者等信息,不验证有效期var claimsPrincipal = tokenHandler.ValidateToken(token, validationParameters, out _);return claimsPrincipal;}catch{// 签名验证失败,令牌无效return null;}
}

最后仅仅是匹配成功的情况下,才能执行刷新accessToken的逻辑。


总结

该方案通过生成关联的accessToken与refreshToken,利用SessionUId实现二者匹配验证,在refreshToken有效期内支持安全刷新 accessToken,同时采用Redis存储refreshToken并可实施滑动过期策略,增强了API鉴权的安全性与用户体验。

http://www.xdnf.cn/news/1396693.html

相关文章:

  • DETR:用Transformer革新目标检测的新范式
  • 基于物联网设计的园林灌溉系统(华为云IOT)_274
  • 从单机到分布式:Python 爬虫架构演进
  • 嵌入式Linux学习 - 数据库开发
  • 系统集成项目管理工程师第十二章:执行过程组全解析
  • 操作系统上的Docker安装指南:解锁容器化新世界
  • 进制转换问题
  • Tomcat 企业级运维实战系列(五):Tomcat 优化和安全加固
  • 简易TCP网络程序
  • 250830-Docker从Rootless到Rootful的Gitlab镜像迁移
  • 【Linux】网络安全管理:Netfilter、nftables 与 Firewalld | Redhat
  • Pmp项目管理方法介绍|权威详解与实战指南
  • 【超全汇总】MySQL服务启动命令手册(Linux+Windows+macOS)(上)
  • MYSQL速通(3/5)
  • Linux 830 shell:expect,ss -ant ,while IFS=read -r line,
  • 构建AI智能体:十八、解密LangChain中的RAG架构:让AI模型突破局限学会“翻书”答题
  • Python自定义函数形式参中的*args、**kwargs、*和/
  • STM32G474 IAP 双bank升级的坑
  • WebStorm无法识别@下的文件,但是可以正常使用
  • 【后端数据库】MySQL 索引生效/失效规则 + 核心原理
  • 腾讯云OpenCloudOS 9系统部署OpenTenBase数据库详细教程
  • 【云原生】Docker 搭建Kafka服务两种方式实战操作详解
  • php连接rabbitmq例子
  • 【序列晋升】21 Spring Cloud Gateway 云原生网关演进之路
  • 卷积神经网络项目:基于CNN实现心律失常(ECG)的小颗粒度分类系统
  • HAProxy 负载均衡全解析:从基础部署、负载策略到会话保持及性能优化指南
  • docker命令(二)
  • 现状摸底:如何快速诊断企业的“数字化健康度”?
  • PCIe 6.0 TLP深度解析:从结构设计到错误处理的全链路机制
  • 算法题(194):字典树