zoukankan      html  css  js  c++  java
  • 翻译一篇英文文章,主要是给自己看的——在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令牌中放入的东西不是秘密的,它不能被篡改,除非使令牌无效。

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

  • 相关阅读:
    ajax实例2
    分页显示中关于"序号"的问题
    <s:property value=""/> 怎么截取返回值的固定长度的字符串
    py 的 第 13 天
    py 的 第 10 天
    py 的 第 8 天
    py 的 第 9 天
    py 的 第 7 天
    这几日英文大汇
    python第五天
  • 原文地址:https://www.cnblogs.com/bamboo-zhang/p/11699772.html
Copyright © 2011-2022 走看看