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 }

会执行IncrementAccessFailedCountAsyncUpdateUserAsync。如果成功执行,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);
    }
}
 

posted @ 2020-11-26 14:20  天命小猪  阅读(1830)  评论(0编辑  收藏  举报