Ultimate ASP.NET CORE 6.0 Web API --- 读书笔记(28)
28 Refresh Token
本文内容来自书籍: Marinko Spasojevic - Ultimate ASP.NET Core Web API - From Zero To Six-Figure Backend Developer (2nd edition)
需要本书和源码的小伙伴可以留下邮箱,有空看到会发送的
在前面的章节中,我们创建了一个流程,实现了用户登录,获取token来访问可以访问的被保护资源,然后在token过期之后,需要用户重新登录来获得一个新的有效token
这个流程非常好,并且已经应用在许多商业应用上,但是,有时候我们不希望,每次在token过期之后强制用户去重新登录,那么为了解决这个问题,我们可以使用refresh token
Refresh token
是一种用来获取新的访问token的凭证
当一个访问token过期了,那么我们可以使用refresh token
来从验证模块获取新的token,一个refresh token
的生命周期一般是比普通的访问token要长得多
那么对于refresh token
的工作流程是这样的:
- 首先,客户端提交凭证来通过验证
- 然后,认证组件就会发放访问token和refresh token
- 接着,客户端通过携带访问token来请求受保护的资源
- 在验证token通过之后,返回受保护的资源
- 这种情况一直持续到访问token过期
- 一旦token过期之后,客户端携带
refresh token
来向认证组件请求一个新的访问token - 然后认证组件会发放一个新的访问token和
refresh token
- 然后一直持续到
refresh token
过期 - 一旦
refresh token
过期,那么客户端就必须重新向认证组件重新认证,也就是重新登录
28.1 Why Do We Need a Refresh Tokem
为什么需要refresh token
,为什么不发放一个长期的token,比如1年或者1个月?
因为,如果我们发放长期的token,那么用户就可以长时间持有这个token而不用登录,即使他的密码已经修改过了
使用refresh token
这个概念是因为,我们可以发放短期的访问token,即使被攻击了,恶意者也只能拿到一个短期的token,在refresh token-based flow
,认证组件服务器会发放一个一次性的refresh token
和一个短期的访问token
app每次发送请求都会在Authorization header
带有短期的token,方便服务器验证,一旦token过期,服务器会返回相关过期信息,然后app接收到这个信息之后,就会带着过期的token和refresh token
去获取新的访问token和refresh token
如果某些时候出现异常了,那么refresh token
可以被撤销,也就是说,即使app带着这个refresh token
想要获取新的访问token,也会被拒绝,然后用户必须重新登录
所以,refresh token
让我们有一个平滑的认证工作流程,而不需要用户频繁地登录,而且又不会破坏应用的安全性
28.2 Refresh Token Implementation
首先,我们需要修改我们User
类,这是实体,所以我们需要更新数据库架构
public class User : IdentityUser
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string? RefreshToken { get; set; }
public DateTime RefreshTokenExpiryTime { get; set; }
}
然后添加DTO
public record TokenDto(string AccessToken, string RefreshToken);
接着,修改认证服务接口
public interface IAuthenticationService
{
Task<IdentityResult> RegisterUser(UserForRegistrationDto userForRegistration);
Task<bool> ValidateUser(UserForAuthenticationDto userForAuth);
Task<TokenDto> CreateToken(bool populateExp);
}
public async Task<TokenDto> CreateToken(bool populateExp)
{
var signingCredentials = GetSigningCredentials();
var claims = await GetClaims();
var tokenOptions = GenerateTokenOptions(signingCredentials, claims);
var refreshToken = GenerateRefreshToken();
_user.RefreshToken = refreshToken;
if (populateExp)
_user.RefreshTokenExpiryTime = DateTime.Now.AddDays(7);
await _userManager.UpdateAsync(_user);
var accessToken = new JwtSecurityTokenHandler().WriteToken(tokenOptions);
return new TokenDto(accessToken, refreshToken);
}
private static string GenerateRefreshToken()
{
var randomNumber = new byte[32];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
然后,我们还需要为过期的token服务,新建一个TokenController
[Route("api/token")]
[ApiController]
public class TokenController : ControllerBase
{
private readonly IServiceManager _service;
public TokenController(IServiceManager service) => _service = service;
[HttpPost("refresh")]
[ServiceFilter(typeof(ValidationFilterAttribute))]
public async Task<IActionResult> Refresh([FromBody] TokenDto tokenDto)
{
var tokenDtoToReturn = await _service.AuthenticationService.RefreshToken(tokenDto);
return Ok(tokenDtoToReturn);
}
}
继续添加认证服务接口
public interface IAuthenticationService
{
Task<IdentityResult> RegisterUser(UserForRegistrationDto userForRegistration);
Task<bool> ValidateUser(UserForAuthenticationDto userForAuth);
Task<TokenDto> CreateToken(bool populateExp);
Task<TokenDto> RefreshToken(TokenDto tokenDto);
}
public async Task<TokenDto> RefreshToken(TokenDto tokenDto)
{
var principal = GetPrincipalFromExpiredToken(tokenDto.AccessToken);
var user = await _userManager.FindByNameAsync(principal.Identity.Name);
if (user == null || user.RefreshToken != tokenDto.RefreshToken ||
user.RefreshTokenExpiryTime <= DateTime.Now)
throw new RefreshTokenBadRequest();
_user = user;
return await CreateToken(populateExp: false);
}
private ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
{
var jwtSettings = _configuration.GetSection("JwtSettings");
var tokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("SECRET") ?? "asdnAAjhaskdijhciwn")),
ValidateLifetime = true,
ValidIssuer = jwtSettings["validIssuer"],
ValidAudience = jwtSettings["validAudience"]
};
var tokenHandler = new JwtSecurityTokenHandler();
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out var securityToken);
if (securityToken is not JwtSecurityToken jwtSecurityToken ||
!jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256,
StringComparison.InvariantCultureIgnoreCase))
{
throw new SecurityTokenException("Invalid token");
}
return principal;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示