翻译一篇英文文章,主要是给自己看的——在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开发人员工具控制台中尝试使用此客户端,则其外观如下:

 

这里要注意的一件事是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令牌中放入的东西不是秘密的,它不能被篡改,除非使令牌无效。

 

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

posted @ 2019-10-19 21:58  南东  阅读(305)  评论(0编辑  收藏  举报