我遇到了一个非常奇怪的问题,让我非常困惑并且一点都不可理喻。
让我来解释一下:我有一个由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类:
这是我的JwtIssuerOptions.cs文件。
让我来解释一下:我有一个由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;
}
}
}