.NET Core 2.0 JWT令牌过期问题

3
我遇到了一个非常奇怪的问题,让我非常困惑并且一点都不可理喻。
让我来解释一下:我有一个由Angular 5客户端调用的.NET Core 2.0 Web API,该Web API托管在Azure AppService中。身份验证使用AspnetCore.Authentication.JWTBearer进行JWT Bearer令牌进行认证(当前版本为2.0.1)。应用程序在auth / login端点中正确创建JWT令牌。然后客户端能够正常通过以下调用进行身份验证。
但是,即使我指定了1080分钟(一周)的令牌持续时间,在大约8小时左右之后,令牌就不再有效了。我可以接受这一点(实际上我开始指定令牌有效期为几个小时),但是一旦令牌过期...这里出现了奇怪的问题,用户登录后应用程序会发放新令牌,但是新令牌无法进行身份验证,提示令牌已过期!它刚被创建怎么可能过期呢?(我已经检查了两次,新收到的令牌被发送到服务器而不是旧令牌)。
此外,如果我只是重新启动Azure中的应用程序服务,那么一切就恢复正常了,并且新发放的jwt令牌得到了接受。我认为这可能与Azure的服务器和其他事物之间的时钟有关,因此我删除了ClockSkew属性并将其留在5分钟,这是它的默认值,但没有运气。
我不知道是什么原因导致这种奇怪的行为,但它使我的应用程序在一天的某个时候变得无用,除非我进入Azure并重新启动应用程序服务。
以下是我的代码,但我开始认为它可能是与.NET Core和Azure相关的错误?您看到了什么问题吗? 感谢您的帮助!
以下是我的startup.cs类:
public class Startup
    {
        private string connectionString;
        private const string SecretKey = "iNivDmHLpUA223sqsfhqGbMRdRj1PVkH"; 
        // todo: get this from somewhere secure
        private readonly SymmetricSecurityKey _signingKey = new 
              SymmetricSecurityKey(Encoding.ASCII.GetBytes(SecretKey));
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            connectionString = Configuration.GetSection("ConnectionString:Value").Value;
            Console.WriteLine("Connection String: " + connectionString);
            services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));

            //Initialize the UserManager instance and configuration
            services.AddIdentity<AppUser, IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();

            services.TryAddTransient<IHttpContextAccessor, HttpContextAccessor>();


            // add identity
            var builder = services.AddIdentityCore<AppUser>(o =>
            {
                // configure identity options
                o.Password.RequireDigit = true;
                o.Password.RequireLowercase = true;
                o.Password.RequireUppercase = true;
                o.Password.RequireNonAlphanumeric = true;
                o.Password.RequiredLength = 6;
            });

            builder = new IdentityBuilder(builder.UserType, typeof(IdentityRole), builder.Services);
            builder.AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();

            //START JWT CONFIGURATION
            services.AddSingleton<IJwtFactory, JwtFactory>();

            // Get options from app settings
            var jwtAppSettingOptions = Configuration.GetSection(nameof(JwtIssuerOptions));

            // Configure JwtIssuerOptions
            services.Configure<JwtIssuerOptions>(options =>
            {
                options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
                options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)];
                options.SigningCredentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256);
            });

            var tokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)],
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = _signingKey,

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

                RequireExpirationTime = false,
                // ValidateLifetime = true,
                // ClockSkew = TimeSpan.Zero //default son 5 minutos
            };

            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(configureOptions =>
            {
                configureOptions.ClaimsIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
                configureOptions.TokenValidationParameters = tokenValidationParameters;
                configureOptions.SaveToken = true;
            });

            // api user claim policy
            // Enables [Authorize] decorator on controllers.
            //more information here: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-2.1
            services.AddAuthorization(options =>
            {
                options.AddPolicy("ApiUser", policy => policy.RequireClaim(Constants.Strings.JwtClaimIdentifiers.Rol, Constants.Strings.JwtClaims.ApiAccess));
            });
            //END JWT CONFIGURATION

            // Register the Swagger generator, defining one or more Swagger documents
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new Info
                {
                    Title = Configuration.GetSection("Swagger:Title").Value,
                    Version = "v1"
                });
            });


            //Initialize auto mapper
            services.AddAutoMapper();

            services.AddCors();

            //Initialize MVC
            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env,
        UserManager<AppUser> userManager, RoleManager<IdentityRole> roleManager)
        {
            var cultureInfo = new CultureInfo("es-AR");
            //cultureInfo.NumberFormat.CurrencySymbol = "€";

            CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
            CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;


            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseExceptionHandler(
          builder =>
          {
              builder.Run(
                        async context =>
                        {
                            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
                            context.Response.Headers.Add("Access-Control-Allow-Origin", "*");

                            var error = context.Features.Get<IExceptionHandlerFeature>();
                            if (error != null)
                            {
                                context.Response.AddApplicationError(error.Error.Message);
                                await context.Response.WriteAsync(error.Error.Message).ConfigureAwait(false);
                            }
                        });
          });

            // Enable middleware to serve generated Swagger as a JSON endpoint.
            app.UseSwagger();

            // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), specifying the Swagger JSON endpoint.
            app.UseSwaggerUI(c =>
            {
                c.SwaggerEndpoint(
                    Configuration.GetSection("Swagger:Endpoint").Value,
                    Configuration.GetSection("Swagger:Title").Value);
            });

            app.UseAuthentication();

            //Loads initial users and roles.
            if (Configuration["seed"] == "true")
            {
                Console.WriteLine("Seeding database with connection string: " + connectionString);
                Console.WriteLine();
                IdentityDataInitializer.SeedData(userManager, roleManager);
                Console.WriteLine("Finished seeding");
            }
            else
            {
                Console.WriteLine("seeding not configured");

            }

            app.UseDefaultFiles();
            app.UseStaticFiles();

            // Shows UseCors with CorsPolicyBuilder.
            app.UseCors(builder =>
               builder.WithOrigins(Configuration.GetSection("AllowedOrigins:Origin1").Value,
                                    Configuration.GetSection("AllowedOrigins:Origin2").Value)
                 .AllowAnyHeader()
                 .AllowAnyMethod() //Permite también PREFLIGHTS / OPTIONS REQUEST!
               );

            Console.WriteLine("Allowed origin: " + Configuration.GetSection("AllowedOrigins:Origin1").Value);
            Console.WriteLine("Allowed origin: " + Configuration.GetSection("AllowedOrigins:Origin2").Value);

            app.UseMvc();
        }

    }

这是我的JwtIssuerOptions.cs文件。
public class JwtIssuerOptions
    {
        /// <summary>
        /// 4.1.1.  "iss" (Issuer) Claim - The "iss" (issuer) claim identifies the principal that issued the JWT.
        /// </summary>
        public string Issuer { get; set; }

        /// <summary>
        /// 4.1.2.  "sub" (Subject) Claim - The "sub" (subject) claim identifies the principal that is the subject of the JWT.
        /// </summary>
        public string Subject { get; set; }

        /// <summary>
        /// 4.1.3.  "aud" (Audience) Claim - The "aud" (audience) claim identifies the recipients that the JWT is intended for.
        /// </summary>
        public string Audience { get; set; }

        /// <summary>
        /// 4.1.4.  "exp" (Expiration Time) Claim - The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing.
        /// </summary>
        public DateTime Expiration => IssuedAt.Add(ValidFor);

        /// <summary>
        /// 4.1.5.  "nbf" (Not Before) Claim - The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing.
        /// </summary>
        public DateTime NotBefore { get; set; } = DateTime.UtcNow;

        /// <summary>
        /// 4.1.6.  "iat" (Issued At) Claim - The "iat" (issued at) claim identifies the time at which the JWT was issued.
        /// </summary>
        public DateTime IssuedAt { get; set; } = DateTime.UtcNow;

        /// <summary>
        /// Set the timespan the token will be valid for (default is 120 min)
        /// </summary>
        public TimeSpan ValidFor { get; set; } = TimeSpan.FromMinutes(1080);//una semana



        /// <summary>
        /// "jti" (JWT ID) Claim (default ID is a GUID)
        /// </summary>
        public Func<Task<string>> JtiGenerator =>
          () => Task.FromResult(Guid.NewGuid().ToString());

        /// <summary>
        /// The signing key to use when generating tokens.
        /// </summary>
        public SigningCredentials SigningCredentials { get; set; }
    }

Token.cs类向客户端发送包含令牌的JSON。

public class Tokens
    {
        public static async Task<object> GenerateJwt(ClaimsIdentity identity, IJwtFactory jwtFactory, string userName, JwtIssuerOptions jwtOptions, JsonSerializerSettings serializerSettings)
        {
            var response = new
            {
                id = identity.Claims.Single(c => c.Type == "id").Value,
                auth_token = await jwtFactory.GenerateEncodedToken(userName, identity),
                expires_in = (int)jwtOptions.ValidFor.TotalSeconds
            };

            return response;
            //return JsonConvert.SerializeObject(response, serializerSettings);
        }
    }

AuthController.cs

[Produces("application/json")]
    [Route("api/[controller]")]
    public class AuthController : Controller
    {
        private readonly UserManager<AppUser> _userManager;
        private readonly IJwtFactory _jwtFactory;
        private readonly JwtIssuerOptions _jwtOptions;
        private readonly ILogger _logger;

        public AuthController(UserManager<AppUser> userManager,
            IJwtFactory jwtFactory,
            IOptions<JwtIssuerOptions> jwtOptions,
            ILogger<AuthController> logger)
        {
            _userManager = userManager;
            _jwtFactory = jwtFactory;
            _jwtOptions = jwtOptions.Value;
            _logger = logger;
        }

        // POST api/auth/login
        [HttpPost("login")]
        public async Task<IActionResult> Post([FromBody]CredentialsViewModel credentials)
        {
            try
            {
                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }

                var identity = await GetClaimsIdentity(credentials.UserName, credentials.Password);
                if (identity == null)
                {
                    // Credentials are invalid, or account doesn't exist
                    _logger.LogInformation(LoggingEvents.InvalidCredentials, "Invalid Credentials");
                    return BadRequest(Errors.AddErrorToModelState("login_failure", "Invalid username or password.", ModelState));
                }

                var jwt = await Tokens.GenerateJwt(identity, _jwtFactory, credentials.UserName, _jwtOptions, new JsonSerializerSettings { Formatting = Formatting.Indented });
                CurrentUser cu = Utils.GetCurrentUserInformation(identity.Claims.Single(c => c.Type == "id").Value, _userManager).Result;
                if (cu != null)
                {
                    cu.Jwt = jwt;
                    return new OkObjectResult(cu);
                }

                return StatusCode(500);
            }
            catch (System.Exception ex)
            {
                _logger.LogError(LoggingEvents.GenericError, ex.Message);
                return StatusCode(500, ex);
            }
        }

        private async Task<ClaimsIdentity> GetClaimsIdentity(string userName, string password)
        {
            try
            {
                if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(password))
                    return await Task.FromResult<ClaimsIdentity>(null);

                // get the user to verifty
                ILogicUsers lusers = Business.UsersLogic(_userManager);
                AppUser userToVerify = await lusers.FindByNameAsync(userName);

                if (userToVerify == null)
                    return await Task.FromResult<ClaimsIdentity>(null);

                // check the credentials
                if (await lusers.CheckPasswordAsync(userToVerify, password))
                {
                    return await Task.FromResult(_jwtFactory.GenerateClaimsIdentity(userName, userToVerify.Id));
                }

                // Credentials are invalid, or account doesn't exist
                _logger.LogInformation(LoggingEvents.InvalidCredentials, "Invalid Credentials");
                return await Task.FromResult<ClaimsIdentity>(null);
            }
            catch
            {
                throw;
            }
        }
    }

4
一周不等于1080分钟,只有18个小时。 - Brad
嗨@Brad,你说得完全正确,我改变了许多次ValidFor时间跨度,以至于我感到困惑。不过,这似乎不是问题所在,因为在18小时后,令牌未经授权,我无法继续使用应用程序(无论客户端浏览器如何),最终我只能重新启动Azure应用服务。 - Yanick Tourn
这是一个本地环境的短视频。http://recordit.co/06qr51XCH0 - Yanick Tourn
您上面发布的令牌看起来不错,IAT和EXP之间相差18小时,应该仍然有效。 您可以在https://jwt.io上检查它-您是否尝试直接使用UtcNow而不是引用issuedAt来计算Expration? - jps
1
好的,我想我找到了问题所在。IssuedAt属性是静态的,并且采用了第一次生成令牌的值。当令牌过期时,会生成一个新的令牌,但是采用了第一个令牌的issuedAt日期,这就是为什么所有新生成的令牌都已过期的原因。在Azure中重新启动AppService会导致静态值被清除,并且第一个新令牌将被正确创建。这是正确的行。 public DateTime IssuedAt => DateTime.UtcNow;感谢您的帮助! - Yanick Tourn
显示剩余4条评论
1个回答

4

好的,我想我已经找出了问题所在。

IssuedAt属性是静态的,并且取第一个生成令牌的时间值。当令牌过期时,就会生成一个新令牌,但是仍然使用第一个令牌的签发时间,这就是为什么所有新生成的令牌都已过期的原因。在Azure中重新启动AppService可以清除静态值,并正确创建第一个新令牌。

这是正确的行。

public DateTime IssuedAt => DateTime.UtcNow; 

感谢您的帮助!

我曾经遇到过完全相同的问题。在“Expiration”-属性中,我只是返回了IssuedAt.Add(ValidFor),这总是让我得到初始值。我想我们都使用了JwtIssuerOptions类的相同代码示例。你从哪里得到的?我们可能应该对此提出改进建议。在我的情况下,一位同事实现了它,所以我现在无法说这是来自哪里。 - Weissvonnix
1
是的,好主意。问题似乎来自于这个教程,https://fullstackmark.com/post/13/jwt-authentication-with-aspnet-core-2-web-api-angular-5-net-core-identity-and-facebook-login,在其中一个部分指向了这个存储库https://github.com/mmacneil/AngularASPNETCore2WebApiAuth/blob/master/src/Models/JwtIssuerOptions.cs。 - Yanick Tourn

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接