新的 JWT 令牌与旧令牌的有效期相同

Posted

技术标签:

【中文标题】新的 JWT 令牌与旧令牌的有效期相同【英文标题】:New JWT token has same expiry as old token 【发布时间】:2018-04-21 15:48:42 【问题描述】:

我已经在我的 ASP.NET Core 2.0 Web API 中生成 JWT 令牌,但我遇到了一个问题,即后续的新访问令牌与之前生成的令牌具有相同的过期时间。

例如,我发布登录凭据,并返回访问令牌。访问令牌在 [Authorize] API 端点上按预期工作。出于测试目的,我将令牌设置为 1 分钟后过期。 1 分钟后,令牌过期,经过身份验证的端点返回 401,如预期的那样。

我正在我的客户端应用程序中处理 401。登录表单出现,用户再次登录。生成并返回一个新令牌。唯一的问题是,这个新令牌与最初生成的令牌具有完全相同的“ValidTo”日期时间。在使用这个新令牌后导致任何调用返回 401,因为令牌已经过期。我已经确认两个不同的令牌正在检查,所以我传递错误的令牌不是问题

第一个令牌失败(预期,因为令牌已过期):

Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler:信息:无法验证令牌 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZGFtQHBytNDRiYS1...Do1NzM5NS8ifQ.t8DjvlGV7GZ3xucwu-1hlJRXA5yQHHPii>>

Microsoft.IdentityModel.Tokens.SecurityTokenExpiredException:IDX10223:生命周期验证失败。令牌已过期。

有效期至:'11/08/2017 19:23:09'

当前时间:'11/08/2017 19:23:13'。

第二个令牌失败(不是预期的,ValidTo 与前一个令牌相同)

Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler:信息:无法验证令牌 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZGFtQ...dDo1NzM5NS8ifQ.2TMPJvYnQl1Jw78M2nj40uD3qejBEciXfKC85saGNI.p>

Microsoft.IdentityModel.Tokens.SecurityTokenExpiredException:IDX10223:生命周期验证失败。令牌已过期。

有效期至:'11/08/2017 19:23:09'

当前时间:'11/08/2017 19:23:34'。

Startup.cs 中的 JWT 配置

services.Configure<JwtIssuerOptions>(options => 
            options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
            options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)];
            options.SigningCredentials = new SigningCredentials(SigningKey, SecurityAlgorithms.HmacSha256);
            options.ValidFor = TimeSpan.FromMinutes(1);
        );
        services.AddAuthentication(o => 
        
            o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        ).AddJwtBearer(o =>
        
            o.TokenValidationParameters = new TokenValidationParameters
            
                ValidateIssuer = true,
                ValidIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)],

                ValidateAudience = true,
                ValidAudience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)],

                ValidateIssuerSigningKey = true,
                IssuerSigningKey = SigningKey,

                RequireExpirationTime = true,
                ValidateLifetime = true,
                ClockSkew = TimeSpan.Zero,
            ;
        );

创建Token的登录动作:

[HttpPost]
    public async Task<IActionResult> Login([FromBody]CredentialsViewModel credentials)
    
        if (!ModelState.IsValid)
        
            return BadRequest(ModelState);
        

        var identity = await GetClaimsIdentity(credentials.UserName, credentials.Password);
        if (identity == null)
        
            return BadRequest(Errors.AddErrorToModelState("login_failure", "Invalid username or password.", ModelState));
        

        // Serialize and return the response
        var response = new
        
            id = identity.Claims.Single(c => c.Type == "id").Value,
            auth_token = await _jwtFactory.GenerateEncodedToken(credentials.UserName, identity),
            expires_in = (int)_jwtOptions.ValidFor.TotalSeconds
        ;

        var json = JsonConvert.SerializeObject(response, _serializerSettings);
        return new OkObjectResult(json);
    

生成令牌的JwtFactory方法:

private readonly JwtIssuerOptions _jwtOptions;

public JwtFactory(IOptions<JwtIssuerOptions> jwtOptions)

   _jwtOptions = jwtOptions.Value;
   ThrowIfInvalidOptions(_jwtOptions);


public async Task<string> GenerateEncodedToken(string userName, ClaimsIdentity identity)
    
        var claims = new[]
     
             new Claim(JwtRegisteredClaimNames.Sub, userName),
             new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()),
             new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), ClaimValueTypes.Integer64),
             identity.FindFirst("rol"),
             identity.FindFirst("id")
         ;

        // Create the JWT security token and encode it.
        var jwt = new JwtSecurityToken(
            issuer: _jwtOptions.Issuer,
            audience: _jwtOptions.Audience,
            claims: claims,
            notBefore: _jwtOptions.NotBefore,
            expires: _jwtOptions.Expiration,
            signingCredentials: _jwtOptions.SigningCredentials);

        var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);

        return encodedJwt;
    

【问题讨论】:

【参考方案1】:

问题出在我的 jwtFactory 中,我在依赖注入 IOptions。由于这是在启动时定义的,并且有几个在创建对象时会自动填充的属性(例如 IssuedAt,它获取 DateTime.NowUtc),所以 IOptions 仅返回第一次加载时的配置。

我能够通过注入 IOptionsSnapshot 来解决这个问题,它会获取 JwtIssuerOptions 的新版本,该版本将具有更新的 IssuedAt 属性。

private readonly JwtIssuerOptions _jwtOptions;

    public JwtFactory(IOptionsSnapshot<JwtIssuerOptions> jwtOptions)
    
        _jwtOptions = jwtOptions.Value;
        ThrowIfInvalidOptions(_jwtOptions);
    

【讨论】:

【参考方案2】:

只是一个建议,_jwtOptions.Expiration 应该是一个时间跨度,即令牌的有效期,所以假设它是 20 分钟,在这种情况下,你应该有 expires: DateTime.UtcNow.AddMinutes(_jwtOptions.Expiration) 或类似的东西。甚至可能将其重命名以反映这一点。

【讨论】:

在我的 JwtIssuerOptions 模型中,Expiration 是一个 lambda,它采用 IssuedAt Datetime 并自动添加 ValidFor 时间跨度。 IssuedAt 只是在模型声明中使用 DateTime.UtcNow 进行初始化。所以在我的例子中,只有 ValidFor 需要在 Startup 中显式设置。 JwtSecurityToken 是 System.IdentityModel.Tokens.Jwt 中的一个类,过期需要 DateTime,而不是 Timespan 我知道,这就是我从该配置值创建 datetime 的原因

以上是关于新的 JWT 令牌与旧令牌的有效期相同的主要内容,如果未能解决你的问题,请参考以下文章

PHP JWT:: **解码** 过期令牌

我需要帮助来验证 jwt 是不是有效或以其他方式创建一个新的,如果发送的令牌不正确,则在 nestjs 中间件中返回错误

JWT/Laravel 延长令牌有效期

JWT 访问令牌和刷新令牌安全性

python jwt中令牌过期时如何提取jwt令牌有效负载

访问令牌和刷新令牌困境 - JWT