翻译一篇英文文章,主要是给自己看的——在ASP.NET Core Web Api中如何刷新token

原文地址 :https://www.blinkingcaret.com/2018/05/30/refresh-tokens-in-asp-net-core-web-api/

先申明,本人英语太菜,每次看都要用翻译软件对着看,太痛苦了,所以才翻译的这篇博客,英语好的自己去看,以下为正文

当使用访问令牌来保护web api时,首先想到的是令牌过期时该怎么办?

您是否再次要求用户提供凭证?这并不是一个好的选择。

这篇博客文章是关于使用refresh令牌来解决这个问题的。特别是在 ASP.NET Core Web Apis 中使用JWT令牌。

首先,这真的是一件大事吗?为什么不直接在访问令牌中设置一个较长的过期日期呢?例如,一个月甚至一年?

因为如果我们这么做了,有人设法拿到了那个token,他们可以用一个月,或者一年。即使你更改了密码。

这是因为,如果令牌的签名有效,服务器将信任它,而使其无效的惟一方法是更改用于签名的密钥,这将导致其他所有人的令牌无效。

那就没得选了。这就引出了使用refresh令牌的想法。

那么刷新令牌是如何工作的呢?

想象一下,当您获得一个访问令牌时,您还会获得另一个一次性使用的令牌:refresh令牌。应用程序存储刷新令牌,然后不去管它。

每当应用程序向服务器发送请求时,它都会发送访问令牌(这里的授权:承载令牌),以便服务器知道您是谁。总有一天令牌会过期,服务器会以某种方式通知您。

当这种情况发生时,您的应用程序将发送过期令牌和刷新令牌,并获取新令牌和刷新令牌。如此重复替换。

如果有可疑的事情发生,刷新令牌可以被撤销,这意味着当应用程序试图使用它来获得一个新的访问令牌时,该请求将被拒绝,用户将不得不输入凭据才能再次登录。

为了明确最后一点,假设应用程序在创建refresh令牌时存储请求的位置(例如,爱尔兰的都柏林)。如果用户可以访问这些信息,如果有一些登录用户不认可的地方,用户可以撤销刷新令牌,这样当访问令牌到期谁使用它将无法继续使用的应用程序。这就是为什么它可能是一个好主意是短暂的(我有访问令牌。有效期为几分钟)。

要使用刷新令牌,我们需要能够做到:

  • 创建访问令牌(我们将在这里使用JWT)
  • 生成、保存、检索和撤销刷新令牌(服务器端)
  • 将过期的JWT令牌和刷新令牌替换为新的JWT令牌和刷新令牌(即刷新JWT令牌)
  • 使用ASP.NET身份验证中间件,用于使用JWT令牌对用户进行身份验证
  • 有一种方法来通知应用程序访问令牌已过期(可选)
  • 当令牌过期时,让客户端透明地获取新令牌

如果您需要关于这些主题的单独信息,请继续。如果你想看到所有这些一起工作,你可以在这里找到一个演示项目(demo project here)。

创建JWT访问令牌

如果你想要一个关于如何在ASP.NET Core中使用JWT的更详细的描述,我建议查看Secure a Web Api in ASP.NET Core。这是一个总结。。

首先需要添加 System.IdentityModel.Tokens.Jwt 包:

$ dotnet add package System.IdentityModel.Tokens.Jwt

创建一个新的JWT令牌:

private string GenerateToken(IEnumerable<Claim> claims)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("the server key used to sign the JWT token is here, use more than 16 chars")); var jwt = new JwtSecurityToken(issuer: "Blinkingcaret",
audience: "Everyone",
claims: claims, //the user's claims, for example new Claim[] { new Claim(ClaimTypes.Name, "The username"), //...
notBefore: DateTime.UtcNow,
expires: DateTime.UtcNow.AddMinutes(5),
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
); return new JwtSecurityTokenHandler().WriteToken(jwt); //the method is called WriteToken but returns a string
}

  

这里我们创建了一个新的jwt令牌,它的过期日期是5分钟,使用HmacSha256进行签名。

生成、保存、检索和撤销刷新令牌

刷新标记必须是惟一的,不可能(或很难)猜测它们。

一个简单的GUID似乎满足这个条件。不幸的是,生成guid的过程不是随机的。这意味着,给定几个guid,您可以很容易地猜测下一个guid。

值得庆幸的是,在ASP中有一个安全的随机数生成器。NET Core,我们可以用它来生成一个唯一的字符串,即使给出其中的几个,也很难预测下一个会是什么样子:

using System.Security.Cryptography;

//...

public string GenerateRefreshToken()
{
var randomNumber = new byte[32];
using (var rng = RandomNumberGenerator.Create()){
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
}

  

这里我们生成一个32字节长的随机数,并将其转换为base64,这样我们就可以将它用作字符串。没有关于长度的指导,除了它应该导致一个唯一的和难以猜测的令牌之外。我选了32,但16也可以。

我们需要在首次生成JWT令牌和“刷新”过期令牌时生成刷新令牌。

每次生成新的刷新令牌时,我们都应该以一种将其链接到发出访问令牌的用户的方式保存。

最简单的方法是在用户表中为refresh标记添加一个额外的列。其结果是只允许用户在一个位置登录(每个用户一次只有一个有效的刷新令牌)。

或者,您可以为每个用户维护多个刷新令牌,并从发起它们的请求中保存地理位置、时间等,以便为用户提供活动报告。

另外,让refresh令牌过期可能是一个好主意,例如在几天之后(必须与refresh令牌一起保存过期日期)。

一定不要忘记的一件事是,在刷新操作中使用刷新标记时删除它,这样它就不能被多次使用。

将过期的JWT和刷新令牌替换为新的JWT令牌和刷新令牌(即刷新JWT令牌)

要从过期的访问令牌获取新的访问令牌,我们需要能够访问令牌内的声明,即使令牌已过期。

当您使用ASP.NET Core 身份验证中间件对使用JWT的用户进行身份验证时,它将向过期令牌返回401响应。

我们需要创建一个允许匿名用户的控制器动作,并接受JWT和refresh令牌。

我们需要创建一个允许匿名用户的控制器动作,并接受JWT和refresh令牌。

在该控制器操作中,我们需要手动验证过期的访问令牌(可以选择忽略令牌生存期),并提取其中包含的关于用户的所有信息。

然后,我们可以使用用户信息来检索存储的刷新令牌。然后,我们可以将存储的refresh令牌与在请求中发送的令牌进行比较。

如果一切正常,我们将创建新的JWT和刷新令牌,保存新的刷新令牌,丢弃旧的,并将新的JWT和刷新令牌发送到客户机。

下面是如何从过期的JWT令牌中检索ClaimsPrincipal形式的用户信息:

private ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
{
var tokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false, //you might want to validate the audience and issuer depending on your use case
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("the server key used to sign the JWT token is here, use more than 16 chars")),
ValidateLifetime = false //here we are saying that we don't care about the token's expiration date
}; var tokenHandler = new JwtSecurityTokenHandler();
SecurityToken securityToken;
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out securityToken);
var jwtSecurityToken = securityToken as JwtSecurityToken;
if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
throw new SecurityTokenException("Invalid token"); return principal;
}

  

上面代码片段中值得注意的部分是,我们在TokenValidationParameters中使用了ValidateLifeTime = false,因此过期的令牌被认为是有效的。此外,我们正在检查用于对令牌进行签名的算法是否符合我们的期望(在本例中为HmacSha256)。

这样做的原因是,理论上可以创建JWT令牌并将签名算法设置为“none”(set the signing algorithm to “none”. )。JWT令牌将是有效的(即使未签名)。通过这种方式使用有效的刷新令牌,就可以将一个假令牌替换为一个真正的JWT令牌。

现在我们只需要控制器的行动(它应该是一个帖子,因为它有副作用,而且令牌太长查询字符串参数):

[HttpPost]
public IActionResult Refresh(string token, string refreshToken)
{
var principal = GetPrincipalFromExpiredToken(token);
var username = principal.Identity.Name;
var savedRefreshToken = GetRefreshToken(username); //retrieve the refresh token from a data store
if (savedRefreshToken != refreshToken)
throw new SecurityTokenException("Invalid refresh token"); var newJwtToken = GenerateToken(principal.Claims);
var newRefreshToken = GenerateRefreshToken();
DeleteRefreshToken(username, refreshToken);
SaveRefreshToken(username, newRefreshToken); return new ObjectResult(new {
token = newJwtToken,
refreshToken = newRefreshToken
});
}

  

上面的片段中有一些假设。我省略了检索、保存和删除,还假设每个用户只有一个刷新令牌,这是最简单的场景。

asp.net core身份验证中间件使用jwt令牌对用户进行身份验证

我们需要配置asp.net core的中间件管道,以便如果请求头部带有有效的 Authorization: Bearer JWT_TOKEN 授权,则用户是“已登录”的(“signed in”

如果您想更深入地讨论如何特别在asp.net core中设置jwt,请查看Secure a Web Api in ASP.NET Core.。

在ASP.NET Core2.0版本之后,我们向管道中添加了一个身份验证中间件,并在startup.cs的configureservices中对其进行配置:

public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
//... services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "bearer";
options.DefaultChallengeScheme = "bearer";
}).AddJwtBearer("bearer", options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("the server key used to sign the JWT token is here, use more than 16 chars")),
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero //the default for this setting is 5 minutes
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
});
}

  

上面代码片段中需要注意的是对onAuthenticationFailed事件的处理。当请求带有过期令牌时,它将向响应添加令牌过期头。客户端可以使用此信息来决定使用刷新令牌。但是,我们可以让客户机在收到401响应时尝试使用刷新令牌。我们将依赖于头部过期令牌恢复博客的post请求(原翻译:我们将依赖于这个博客文章的其余部分的响应中的令牌过期头)。

现在我们只需要将身份验证中间件添加到管道:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
} app.UseAuthentication();
//...

 客户端

这里的目标是构建一个api客户机,该客户机可以意识到令牌何时过期,并采取适当的操作来获取新令牌,并透明地执行所有这些操作。

当请求因访问令牌过期而失败时,应使用访问和刷新令牌将新请求发送到刷新端点。在该请求完成并且客户端获得新的令牌之后,应该重复原始请求。

实现这一点将取决于您使用的客户机类型。这里我们将描述一个可能的javascript客户端。我们将依赖于对请求的响应,该请求具有一个名为“token expired”的头的过期令牌。我们将使用fetch来执行对web api的请求。

async function fetchWithCredentials(url, options) {
var jwtToken = getJwtToken();
options = options || {};
options.headers = options.headers || {};
options.headers['Authorization'] = 'Bearer ' + jwtToken;
var response = await fetch(url, options);
if (response.ok) { //all is good, return the response
return response;
} if (response.status === 401 && response.headers.has('Token-Expired')) {
var refreshToken = getRefreshToken(); var refreshResponse = await refresh(jwtToken, refreshToken);
if (!refreshResponse.ok) {
return response; //failed to refresh so return original 401 response
}
var jsonRefreshResponse = await refreshResponse.json(); //read the json with the new tokens saveJwtToken(jsonRefreshResponse.token);
saveRefreshToken(jsonRefreshResponse.refreshToken);
return await fetchWithCredentials(url, options); //repeat the original request
} else { //status is not 401 and/or there's no Token-Expired header
return response; //return the original 401 response
}
}

  

 

在上面的代码片段中有getjwttoken、getrefreshtoken、savejwttoken和saverefreshttoken。在浏览器中,它们将使用浏览器的本地存储来保存和检索令牌,

例如:

function getJwtToken() {
return localStorage.getItem('token');
} function getRefreshToken() {
return localStorage.getItem('refreshToken');
} function saveJwtToken(token) {
localStorage.setItem('token', token);
} function saveRefreshToken(refreshToken) {
localStorage.setItem('refreshToken', refreshToken);
}

还有刷新功能。此函数执行对API终结点的POST请求以刷新令牌,例如,如果该终结点位于/token/refresh:

async function refresh(jwtToken, refreshToken) {
return fetch('token/refresh', {
method: 'POST',
body: `token=${encodeURIComponent(jwtToken)}&refreshToken=${encodeURIComponent(getRefreshToken())}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
}

  如果您在chrome开发人员工具控制台中尝试使用此客户端,则其外观如下:

翻译一篇英文文章,主要是给自己看的——在ASP.NET Core Web Api中如何刷新token

翻译一篇英文文章,主要是给自己看的——在ASP.NET Core Web Api中如何刷新token

这里要注意的一件事是401在控制台中显示为红色。当请求因令牌过期而失败时会发生这种情况。

如果您希望避免看到“错误”(引号中的错误,因为它是有效的状态代码,在本例中是适当的),则可以在javascript中访问令牌的到期日期,并在到期前刷新它。

jwt令牌有3个部分,由一个“.”分隔。第二部分包含用户的声明,其中有一个名为exp的声明,其中包含令牌过期时的unix时间戳。这是如何获取带有jwt令牌到期日期的javascript日期对象的方法:

var claims = JSON.parse(atob(token.split('.')[1]));
var expirationDate = new Date(claims.exp*1000); //unix timestamp is in seconds, javascript in milliseconds

 

检查到期日期感觉很复杂,所以我不建议这样做(同时处理时区可能是个问题)。我决定提到这一点是因为这是一件很有趣的事情,这让我们很清楚,你在jwt令牌中放入的东西不是秘密的,它不能被篡改,除非使令牌无效。

希望你觉得这篇文章有趣。如果是这样的话,在评论中放一行。

上一篇:ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程


下一篇:在 ASP.NET Core Web API中使用 Polly 构建弹性容错的微服务