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的工作流程是这样的:

  1. 首先,客户端提交凭证来通过验证
  2. 然后,认证组件就会发放访问token和refresh token
  3. 接着,客户端通过携带访问token来请求受保护的资源
  4. 在验证token通过之后,返回受保护的资源
  5. 这种情况一直持续到访问token过期
  6. 一旦token过期之后,客户端携带refresh token来向认证组件请求一个新的访问token
  7. 然后认证组件会发放一个新的访问token和refresh token
  8. 然后一直持续到refresh token过期
  9. 一旦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;
}
posted @   huang1993  阅读(63)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示
点击右上角即可分享
微信分享提示