闲着无聊的我又来了,今天介绍一下访问令牌(AccessToken)与刷新令牌(RefreshToken)。
最近公司来了几个Java,然后空气顿时充满了学术的氛围,每天都弥漫着垃圾回收,内存分配,堆栈等等各种技术问题的讨论,竖起了我的小耳朵,我偶然间听到了这个Token过期的问题,今天来说一说。
众所周知,JWT是我们常用的Token类型之一,但是JWT是无状态的,所有的信息,包括超时时间都会编码在JWT中。如果Token设置的超时时间过长会显得有些不安全,如果设置的过短则必须频繁的登录。
在IdentityServer4中,我们可以通过设置AllowOfflineAccess = true为Client设置一个刷新令牌,该令牌可以在Token失效返回401的时候去访问刷新接口,从而获得一个新的访问令牌和一个刷新令牌,但是我们的项目中好像没有用IdentityServer4啊,往下看。
1、打开我们之前的NET6.API项目,我们在LoginView中增加一个字段RefreshToken,并且在登录的时候生成一个Guid(你可以随意定义)作为RefreshToken返回给客户端,通过cache或者redis将该RefreshToken缓存起来,设置一个较长的缓存时间,例如30天。
2、调用登录接口,发现除了AccessToken,RefreshToken也返回了。
3、接着我们新增一个接口,叫做刷新访问令牌。
/// <summary>
/// 刷新访问令牌
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost("refresh")]
[ProducesResponseType(typeof(LoginView), StatusCodes.Status200OK)]
public async Task<IActionResult> RefreshAsync(RefreshTokenDto dto)
{
try
{
var jwtToken = new JwtSecurityTokenHandler().ReadJwtToken(dto.AccessToken);
var userid = jwtToken.Claims.FirstOrDefault(a => a.Type == ClaimTypes.NameIdentifier)?.Value;
var username = jwtToken.Claims.FirstOrDefault(a => a.Type == ClaimTypes.Name)?.Value;
var refreshtoken = CacheHelper.Get<string>($"{CacheEnum.刷新令牌}_{userid}");
if (refreshtoken == null) return Ok(JsonView("未找到该刷新令牌"));
if (refreshtoken != dto.RefreshToken) return Ok(JsonView("刷新令牌不正确"));
#region 签发JWT
//生成一个新的刷新令牌
refreshtoken = CommonFun.GUID;
CacheHelper.Set($"{CacheEnum.刷新令牌}_{userid}", refreshtoken, TimeSpan.FromDays(30));
var view = new LoginView
{
Expires = DateTime.Now.AddDays(7),
RefreshToken = refreshtoken
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["JwtSecurityKey"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: "net6api.com",
audience: "net6api.com",
claims: jwtToken.Claims,
expires: view.Expires,
signingCredentials: creds);
view.AccessToken = new JwtSecurityTokenHandler().WriteToken(token);
return Ok(JsonView(view));
#endregion
}
catch (Exception)
{
return Ok(JsonView("访问令牌解析失败"));
}
}
4、我们调用刷新访问令牌接口,传入旧的AccessToken和RefreshToken,发现接口已经可以成功返回了一组新的Token。
【注意】重点来了!重点来了!重点来了!因为无论是Token是失效还是过期,API返回的也都是401未授权状态,那么前端如何能知道当前的AccessToken是失效还是过期呢,我们可以通过以下手段来实现
打开Program,在AddAuthentication中新增一个JwtBearerEvents,用来监听Toke过期事件,并且往Header中添加一个自定义keyValu,然后我们调整Token的失效时间为5秒,再次尝试。
至此,前端可以通过Response中是否存在自定义Header来判断当前AccessToken是否过期。
前端逻辑如下:
调用login接口登录获取AccessToken和RefreshToken,然后各种调接口…
突然发现HTTPStateCode返回401,此时判断是否存在自定义Header,如果存在则调用refresh刷新Token,继续各种调接口…
如果不存在自定义Header,则重新调用login接口进行登录。
结束,继续摸鱼…