解决SQL Server与.NET中Identity Framework的时间差问题
ASP.NET Identity 锁定时间与系统时间差30分钟问题解决
问题核心
使用_userManager.GetLockoutEndDateAsync(user)获取的用户锁定结束时间,与DateTime.Now始终存在30分钟差异,已确认SQL Server服务器时间准确。
1. 如何确保锁定结束时间与系统时间一致?
- 统一使用UTC时间:所有时间操作优先用
DateTime.UtcNow代替DateTime.Now,Identity的LockoutEnd字段默认存储UTC时间。对比时,将LockoutEnd的UTC值与DateTime.UtcNow对比,而非本地时间:var lockoutEnd = await _userManager.GetLockoutEndDateAsync(user); if (lockoutEnd.HasValue && lockoutEnd.Value.UtcDateTime > DateTime.UtcNow) { // 账户仍处于锁定状态 } - 正确转换时区:如果需要向用户展示本地时间,将UTC时间转换为对应时区,而非直接用本地时间与UTC存储值对比:
var localLockoutEnd = lockoutEnd.Value.LocalDateTime; // 转换为应用服务器本地时区 - 修正异步调用:你的代码中直接调用
GetLockoutEndDateAsync(user)后用Result,存在线程阻塞风险,应改为异步等待:var lockOutEndDate = await _userManager.GetLockoutEndDateAsync(user);
2. Identity Framework或SQL Server是否有导致差异的配置?
- Identity Lockout配置:检查Identity的锁定时间配置,默认
DefaultLockoutTimeSpan为5分钟,但如果自定义了该值,可能导致锁定时长不符合预期。在Program.cs/Startup.cs中查看:services.AddDefaultIdentity<IdentityUser>(options => options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30)) .AddEntityFrameworkStores<ApplicationDbContext>(); - SQL Server时区与字段类型:SQL Server的
datetimeoffset类型会存储时区信息,Identity默认用该类型存储LockoutEnd。如果应用服务器时区与UTC相差30分钟(如部分东南亚时区),直接用DateTime.Now(本地时间)对比LockoutEnd的UTC值会产生差异。 - 用户模型定义:确认你的用户模型中
LockoutEnd字段是DateTimeOffset?类型,而非DateTime?,避免时区信息丢失。
3. 有效排查解决步骤
- 直接验证数据库存储值:查询
AspNetUsers表的LockoutEnd字段,查看实际存储的UTC时间,与应用服务器的DateTime.UtcNow对比,确认是否一致。 - 检查应用服务器时区:在代码中输出时区信息,确认本地时间与UTC的差值:
_logger.LogInformation($"服务器时区:{TimeZoneInfo.Local.Id}"); _logger.LogInformation($"UTC时间:{DateTime.UtcNow}"); _logger.LogInformation($"本地时间:{DateTime.Now}"); _logger.LogInformation($"差值:{DateTime.Now - DateTime.UtcNow}"); - 核对Identity Lockout配置:确认
Lockout.DefaultLockoutTimeSpan、MaxFailedAccessAttempts等参数是否符合预期,是否有自定义的锁定逻辑覆盖默认行为。 - 修正时间对比逻辑:将所有时间对比操作统一为UTC时间,避免时区转换错误。
- 测试锁定流程:故意输错密码触发锁定,记录锁定时的UTC时间,对比数据库中
LockoutEnd的值是否为当前UTC时间加上DefaultLockoutTimeSpan。
附优化后的Login方法代码
public async Task<IActionResult> Login([FromBody] UserForAuthenticationDto userForAuthentication) { try { _logger.LogInformation("Login Method execution started"); var user = await _userManager.FindByNameAsync(userForAuthentication.Email); if (user == null) { _logger.LogError("User Email not found:" + userForAuthentication.Email); throw new Exception(ExceptionsConst.B0000049); } var token = ""; if (await _userManager.IsLockedOutAsync(user)) { var lockOutEndDate = await _userManager.GetLockoutEndDateAsync(user); if (!System.IO.Directory.Exists(EmailTemplatesFolderPath)) { EmailTemplatesFolderPath = _emailTemplate.EmailTemplateFolderPath; } string FileName = Path.Combine(EmailTemplatesFolderPath, "Template_AccountLockedInfo.html"); if (!System.IO.File.Exists(FileName)) { FileName = Path.Combine(EmailTemplatesFolderPath, "Template_AccountLockedInfo.html"); } var html = new HtmlDocument(); html.Load(FileName); var titleNode = html.DocumentNode.SelectSingleNode("//title"); string title = titleNode?.InnerText ?? ""; string EmailContent = System.IO.File.ReadAllText(FileName); EmailContent = EmailContent.Replace("<%username%>", user.UserName); if (lockOutEndDate.HasValue) { EmailContent = EmailContent.Replace("<%lockoutenddate%>", lockOutEndDate.Value.LocalDateTime.ToString("dd/MM/yyyy hh:mm:ss tt K")); } else { EmailContent = EmailContent.Replace("<%lockoutenddate%>", ""); } var message = new Message(user.Email, title, EmailContent, null); await _emailSender.SendEmailAsync(message); throw new Exception(ExceptionsConst.B0000051); } if (!await _userManager.CheckPasswordAsync(user, userForAuthentication.Password)) { await _userManager.AccessFailedAsync(user); _logger.LogError("Invalid password for user:" + userForAuthentication.Email); throw new Exception(ExceptionsConst.B0000049); } if (!await _userManager.IsEmailConfirmedAsync(user)) { _logger.LogError("Email is not confirmed"); throw new Exception(ExceptionsConst.B0000050); } if (!user.IsAccountActive) { _logger.LogError("User account is inactive"); throw new Exception(ExceptionsConst.B0000052); } if (await _userManager.GetTwoFactorEnabledAsync(user)) { Request.Headers.TryGetValue("Trusteddevicetoken", out var TrustedDeviceToken); if (!string.IsNullOrEmpty(TrustedDeviceToken)) { _logger.LogError("TrustedDeviceToken is : " + TrustedDeviceToken); string _userID = _accountsService.GettrustedDevices(TrustedDeviceToken); _logger.LogError("UserID is : " + _userID); if (_userID != user.Id) { return await GenerateOTPFor2StepVerification(user); } } else { _logger.LogError("TrustedDeviceToken is Empty, Generating OTP "); return await GenerateOTPFor2StepVerification(user); } } bool changePasswordRequired = user.ChangePasswordOnNextLogin; user.LastLoginDate = DateTime.UtcNow; await _userManager.UpdateAsync(user); IList<string> roles = await _userManager.GetRolesAsync(user); token = await _jwtHandler.GenerateToken(user); await _userManager.ResetAccessFailedCountAsync(user); var cacheKey = $"LastActivity_{token}"; var cacheEntryOptions = new MemoryCacheEntryOptions() .SetSlidingExpiration(TimeSpan.FromMinutes(Convert.ToDouble(_jwtHandler._jwtSettings["UserIdleTimeOutInMinutes"]))) .SetAbsoluteExpiration(TimeSpan.FromMinutes(Convert.ToDouble(_jwtHandler._jwtSettings["expiryInMinutes"]))); Global.memoryCache.Set(cacheKey, DateTime.UtcNow, cacheEntryOptions); AuditLogger auditLogger = new AuditLogger(); string source = getIPAddress(); auditLogger.LogAudit(UserOperation.AUDIT_USERLOGIN, $"{user.UserName} successfully logged into the system", userForAuthentication.Email, userForAuthentication.Email, ObjectType.USER, source, null, user.Id); return Ok(new AuthResponseDto { FirstName = user.FirstName, LastName = user.LastName, Role = String.Join(",", roles), IsAuthSuccessful = true, Token = token, ChangePassword = changePasswordRequired }); } catch (SqlException sqlex) { _logger.LogError("SQL Exception while login " + sqlex.ToString()); string errorMessage = ExceptionsConst.B0000020.Replace("\n", "").Replace("\r", ""); var resp = new HttpResponseMessage(HttpStatusCode.NoContent) { Content = new StringContent(errorMessage), ReasonPhrase = GetGeneralError(errorMessage) }; return UnprocessableEntity(resp); } catch (Exception ex) { string errorMessage = ex.Message.Replace("\n", "").Replace("\r", ""); _logger.LogInformation("Error occurred while login" + ex.ToString()); var resp = new HttpResponseMessage(HttpStatusCode.NoContent) { Content = new StringContent(errorMessage), ReasonPhrase = GetGeneralError(errorMessage) }; return UnprocessableEntity(resp); } }
内容的提问来源于stack exchange,提问作者Puchakayala Manoj
相关产品推荐
相关产品推荐

