ABP登录返回错误次数、锁定时间
ABP默认登录返回错误结果时,不会显示错误次数、锁定时间。为了实现验证错误时返回错误次数、锁定时间,我们需要改造返回接口。
1.定位验证错误的地方:
修改部分代码
1 /// <summary> 2 /// 获取登录结果,如果错误则返回错误信息 3 /// </summary> 4 /// <param name="usernameOrEmailAddress"></param> 5 /// <param name="password"></param> 6 /// <param name="tenancyName"></param> 7 /// <returns></returns> 8 private async Task<AbpLoginResult<Tenant, User>> GetLoginResultAsync(string usernameOrEmailAddress, string password, string tenancyName) 9 { 10 var loginResult = await _logInManager.LoginAsync(usernameOrEmailAddress, password, tenancyName); 11 switch (loginResult.Result) 12 { 13 case AbpLoginResultType.Success: 14 return loginResult; 15 default: 16 { 17 throw _abpLoginResultTypeHelper.CreateExceptionForFailedLoginAttempt(loginResult.Result, usernameOrEmailAddress, tenancyName, loginResult.User); 18 } 19 } 20 }
2.修改CreateExceptionForFailedLoginAttempt方法:
1 public Exception CreateExceptionForFailedLoginAttempt(AbpLoginResultType result, string usernameOrEmailAddress, string tenancyName, User user) 2 { 3 switch (result) 4 { 5 case AbpLoginResultType.Success: 6 return new Exception("Don't call this method with a success result!"); 7 case AbpLoginResultType.InvalidUserNameOrEmailAddress: 8 case AbpLoginResultType.InvalidPassword: 9 //return new UserFriendlyException(L("LoginFailed"), L("InvalidUserNameOrPassword")); 10 return new UserFriendlyException(L("LoginFailed"), L("InvalidUserNameOrPasswordRemainErrorTimes{0}", 5 - user.AccessFailedCount)); 11 case AbpLoginResultType.InvalidTenancyName: 12 return new UserFriendlyException(L("LoginFailed"), L("ThereIsNoTenantDefinedWithName{0}", tenancyName)); 13 case AbpLoginResultType.TenantIsNotActive: 14 return new UserFriendlyException(L("LoginFailed"), L("TenantIsNotActive", tenancyName)); 15 case AbpLoginResultType.UserIsNotActive: 16 return new UserFriendlyException(L("LoginFailed"), L("UserIsNotActiveAndCanNotLogin", usernameOrEmailAddress)); 17 case AbpLoginResultType.UserEmailIsNotConfirmed: 18 return new UserFriendlyException(L("LoginFailed"), L("UserEmailIsNotConfirmedAndCanNotLogin")); 19 case AbpLoginResultType.LockedOut: 20 //todo 此处后期需要改为客户端获取UTC时间后,格式化展示,以符合国际化 21 return new UserFriendlyException(L("LoginFailed"), L("UserLockedOutMessageUntilTime{0}", user.LockoutEndDateUtc?.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"))); 22 default: // Can not fall to default actually. But other result types can be added in the future and we may forget to handle it 23 Logger.Warn("Unhandled login fail reason: " + result); 24 return new UserFriendlyException(L("LoginFailed")); 25 } 26 }
3.原以为这样就可以使用了,调试时候发现数据库更新了,但是loginResult.User的结果不是最新的。试过各种方式:从UserStore、IRepository<User, long>、DBContext中获取,都是和loginResult.User一致,但是和数据库不一致。
4.后续定位问题,发现执行完
var loginResult = await _logInManager.LoginAsync(usernameOrEmailAddress, password, tenancyName);
后,数据库就更新了。为了弄清楚原理只能去解读ABP源码了。
5.一路追查,后来定位到如下TryLockOutAsync方法,在AbpLogInManager类中
protected virtual async Task<bool> TryLockOutAsync(int? tenantId, long userId) { using (var uow = UnitOfWorkManager.Begin(TransactionScopeOption.Suppress)) { using (UnitOfWorkManager.Current.SetTenantId(tenantId)) { var user = await UserManager.FindByIdAsync(userId.ToString()); (await UserManager.AccessFailedAsync(user)).CheckErrors(); var isLockOut = await UserManager.IsLockedOutAsync(user); await UnitOfWorkManager.Current.SaveChangesAsync(); await uow.CompleteAsync(); return isLockOut; } } }
大家可以看到此方法会重新从UserManager中获取user对象,并返回isLockOut。而UserManager.AccessFailedAsync如下:
1 /// <summary> 2 /// Increments the access failed count for the user as an asynchronous operation. 3 /// If the failed access account is greater than or equal to the configured maximum number of attempts, 4 /// the user will be locked out for the configured lockout time span. 5 /// </summary> 6 /// <param name="user">The user whose failed access count to increment.</param> 7 /// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/> of the operation.</returns> 8 public virtual async Task<IdentityResult> AccessFailedAsync(TUser user) 9 { 10 ThrowIfDisposed(); 11 var store = GetUserLockoutStore(); 12 if (user == null) 13 { 14 throw new ArgumentNullException(nameof(user)); 15 } 16 17 // If this puts the user over the threshold for lockout, lock them out and reset the access failed count 18 var count = await store.IncrementAccessFailedCountAsync(user, CancellationToken); 19 if (count < Options.Lockout.MaxFailedAccessAttempts) 20 { 21 return await UpdateUserAsync(user); 22 } 23 Logger.LogWarning(12, "User is locked out."); 24 await store.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.Add(Options.Lockout.DefaultLockoutTimeSpan), 25 CancellationToken); 26 await store.ResetAccessFailedCountAsync(user, CancellationToken); 27 return await UpdateUserAsync(user); 28 }
会执行IncrementAccessFailedCountAsync和UpdateUserAsync。如果成功执行,user会增加一次验证失败的统计并保存到数据库中。
TryLockOutAsync被AbpLogInManager类中的LoginAsyncInternal方法所调用。我们需要改写此方法。
1 protected virtual async Task<AbpLoginResult<TTenant, TUser>> LoginAsyncInternal(string userNameOrEmailAddress, string plainPassword, string tenancyName, bool shouldLockout) 2 { 3 if (userNameOrEmailAddress.IsNullOrEmpty()) 4 { 5 throw new ArgumentNullException(nameof(userNameOrEmailAddress)); 6 } 7 8 if (plainPassword.IsNullOrEmpty()) 9 { 10 throw new ArgumentNullException(nameof(plainPassword)); 11 } 12 13 //Get and check tenant 14 TTenant tenant = null; 15 using (UnitOfWorkManager.Current.SetTenantId(null)) 16 { 17 if (!MultiTenancyConfig.IsEnabled) 18 { 19 tenant = await GetDefaultTenantAsync(); 20 } 21 else if (!string.IsNullOrWhiteSpace(tenancyName)) 22 { 23 tenant = await TenantRepository.FirstOrDefaultAsync(t => t.TenancyName == tenancyName); 24 if (tenant == null) 25 { 26 return new AbpLoginResult<TTenant, TUser>(AbpLoginResultType.InvalidTenancyName); 27 } 28 29 if (!tenant.IsActive) 30 { 31 return new AbpLoginResult<TTenant, TUser>(AbpLoginResultType.TenantIsNotActive, tenant); 32 } 33 } 34 } 35 36 var tenantId = tenant == null ? (int?)null : tenant.Id; 37 using (UnitOfWorkManager.Current.SetTenantId(tenantId)) 38 { 39 await UserManager.InitializeOptionsAsync(tenantId); 40 41 //TryLoginFromExternalAuthenticationSources method may create the user, that's why we are calling it before AbpUserStore.FindByNameOrEmailAsync 42 var loggedInFromExternalSource = await TryLoginFromExternalAuthenticationSourcesAsync(userNameOrEmailAddress, plainPassword, tenant); 43 44 var user = await UserManager.FindByNameOrEmailAsync(tenantId, userNameOrEmailAddress); 45 if (user == null) 46 { 47 return new AbpLoginResult<TTenant, TUser>(AbpLoginResultType.InvalidUserNameOrEmailAddress, tenant); 48 } 49 50 if (await UserManager.IsLockedOutAsync(user)) 51 { 52 return new AbpLoginResult<TTenant, TUser>(AbpLoginResultType.LockedOut, tenant, user); 53 } 54 55 if (!loggedInFromExternalSource) 56 { 57 if (!await UserManager.CheckPasswordAsync(user, plainPassword)) 58 { 59 if (shouldLockout) 60 { 61 if (await TryLockOutAsync(tenantId, user.Id)) 62 { 63 return new AbpLoginResult<TTenant, TUser>(AbpLoginResultType.LockedOut, tenant, user); 64 } 65 } 66 67 return new AbpLoginResult<TTenant, TUser>(AbpLoginResultType.InvalidPassword, tenant, user); 68 } 69 70 await UserManager.ResetAccessFailedCountAsync(user); 71 } 72 73 return await CreateLoginResultAsync(user, tenant); 74 } 75 }
6.在LogInManager中override LoginAsyncInternal方法,并添加新的TryLockOutAsync方法,传入User引用,在uow成功提交后,赋值AccessFailedCount 和 LockoutEndDateUtc 属性。这样loginResult.User即可保持最新。
public class LogInManager : AbpLogInManager<Tenant, Role, User> { public LogInManager( UserManager userManager, IMultiTenancyConfig multiTenancyConfig, IRepository<Tenant> tenantRepository, IUnitOfWorkManager unitOfWorkManager, ISettingManager settingManager, IRepository<UserLoginAttempt, long> userLoginAttemptRepository, IUserManagementConfig userManagementConfig, IIocResolver iocResolver, IPasswordHasher<User> passwordHasher, RoleManager roleManager, UserClaimsPrincipalFactory claimsPrincipalFactory) : base( userManager, multiTenancyConfig, tenantRepository, unitOfWorkManager, settingManager, userLoginAttemptRepository, userManagementConfig, iocResolver, passwordHasher, roleManager, claimsPrincipalFactory) { } protected override async Task<AbpLoginResult<Tenant, User>> LoginAsyncInternal(string userNameOrEmailAddress, string plainPassword, string tenancyName, bool shouldLockout) { //return base.LoginAsyncInternal(userNameOrEmailAddress, plainPassword, tenancyName, shouldLockout); if (userNameOrEmailAddress.IsNullOrEmpty()) { throw new ArgumentNullException(nameof(userNameOrEmailAddress)); } if (plainPassword.IsNullOrEmpty()) { throw new ArgumentNullException(nameof(plainPassword)); } //Get and check tenant Tenant tenant = null; using (UnitOfWorkManager.Current.SetTenantId(null)) { if (!MultiTenancyConfig.IsEnabled) { tenant = await GetDefaultTenantAsync(); } else if (!string.IsNullOrWhiteSpace(tenancyName)) { tenant = await TenantRepository.FirstOrDefaultAsync(t => t.TenancyName == tenancyName); if (tenant == null) { return new AbpLoginResult<Tenant, User>(AbpLoginResultType.InvalidTenancyName); } if (!tenant.IsActive) { return new AbpLoginResult<Tenant, User>(AbpLoginResultType.TenantIsNotActive, tenant); } } } var tenantId = tenant == null ? (int?)null : tenant.Id; using (UnitOfWorkManager.Current.SetTenantId(tenantId)) { await UserManager.InitializeOptionsAsync(tenantId); //TryLoginFromExternalAuthenticationSources method may create the user, that's why we are calling it before AbpUserStore.FindByNameOrEmailAsync var loggedInFromExternalSource = await TryLoginFromExternalAuthenticationSourcesAsync(userNameOrEmailAddress, plainPassword, tenant); var user = await UserManager.FindByNameOrEmailAsync(tenantId, userNameOrEmailAddress); if (user == null) { return new AbpLoginResult<Tenant, User>(AbpLoginResultType.InvalidUserNameOrEmailAddress, tenant); } if (await UserManager.IsLockedOutAsync(user)) { return new AbpLoginResult<Tenant, User>(AbpLoginResultType.LockedOut, tenant, user); } if (!loggedInFromExternalSource) { if (!await UserManager.CheckPasswordAsync(user, plainPassword)) { if (shouldLockout) { //此处返回修改后的结果,可能会对数据产生影响 if (await TryLockOutAsync(tenantId, user)) { return new AbpLoginResult<Tenant, User>(AbpLoginResultType.LockedOut, tenant, user); } } return new AbpLoginResult<Tenant, User>(AbpLoginResultType.InvalidPassword, tenant, user); } await UserManager.ResetAccessFailedCountAsync(user); } return await CreateLoginResultAsync(user, tenant); } } /// <summary> /// 尝试锁定用户,并更新其状态 /// </summary> /// <param name="tenantId"></param> /// <param name="inputUser"></param> /// <returns></returns> protected async Task<bool> TryLockOutAsync(int? tenantId, User inputUser) { using (var uow = UnitOfWorkManager.Begin(TransactionScopeOption.Suppress)) { using (UnitOfWorkManager.Current.SetTenantId(tenantId)) { var user = await UserManager.FindByIdAsync(inputUser.Id.ToString()); (await UserManager.AccessFailedAsync(user)).CheckErrors(); var isLockOut = await UserManager.IsLockedOutAsync(user); await UnitOfWorkManager.Current.SaveChangesAsync(); await uow.CompleteAsync(); inputUser.AccessFailedCount = user.AccessFailedCount; inputUser.LockoutEndDateUtc = user.LockoutEndDateUtc; return isLockOut; } } //return base.TryLockOutAsync(tenantId, userId); } }