1. 程式人生 > 實用技巧 >ABP登入返回錯誤次數、鎖定時間

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);
    }
}