当前位置: 首页 > 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.dtcms.com/a/358284.html

相关文章:

  • OpenCV 图像形态学操作与边缘检测实战指南
  • ESPTimer vs GPTimer:ESP32 定时器系统深度解析
  • 机器学习 - Kaggle项目实践(6)Dogs vs. Cats Redux: Kernels Edition 猫狗二分类
  • 最强分布式锁工具:Redisson
  • Git 的核心工作流程(三区域模型)
  • github同一台电脑支持两个或以上的ssh账户(macos或Linux系统),解决Key is already in use问题
  • 医院排班|医护人员排班系统|基于springboot医护人员排班系统设计与实现(源码+数据库+文档)
  • 苍穹外卖Day7 | 缓存商品、购物车、SpringCache、缓存雪崩、缓存套餐
  • SpringCloud Alibaba微服务--Sentinel的使用
  • docker 部署Skywalking
  • 基于大模型与 PubMed 检索的光谱数据分析系统
  • 大语言模型的“可解释性”探究——李宏毅大模型2025第三讲笔记
  • Java类加载与JVM详解:从基础到双亲委托机制
  • idea 普通项目转换成spring boot项目
  • Python实现半角数字转全角数字的完整教程
  • 《中国棒垒球》垒球世界纪录多少米·垒球8号位
  • Visual Studio(vs)免费版下载安装C/C++运行环境配置
  • LeetCode 287.寻找重复数
  • Java试题-选择题(23)
  • 【LeetCode 热题 100】62. 不同路径——(解法四)组合数学
  • 聊一聊 .NET 的 AssemblyLoadContext 可插拔程序集
  • rhel-server-7.9-x86_64-dvd.iso
  • 机器学习中KNN算法介绍
  • 笔记共享平台|基于Java+vue的读书笔记共享平台系统(源码+数据库+文档)
  • 数据库原理及应用_数据库基础_第3章数据库编程_常用系统函数
  • 骑行商城怎么开发
  • 【金仓数据库产品体验官】KingbaseES-ORACLE兼容版快速体验
  • 国家统计局数据分析01——机器学习
  • GD32VW553-IOT 基于 vscode 的 bootloader 移植(基于Cmake)
  • 【DreamCamera2】相机应用修改成横屏后常见问题解决方案