原文地址 :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开发人员工具控制台中尝试使用此客户端,则其外观如下:
这里要注意的一件事是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令牌中放入的东西不是秘密的,它不能被篡改,除非使令牌无效。
希望你觉得这篇文章有趣。如果是这样的话,在评论中放一行。