ABP+AdminLTE+Bootstrap Table權限管理系統第八節--ABP錯誤機制及AbpSession相關


  返回總目錄:ABP+AdminLTE+Bootstrap Table權限管理系統一期

     上一節我們講到登錄邏輯,我做的登錄邏輯很簡單的,我們來看一下abp module-zero里面的登錄代碼.

 #region Login / Logout

        public ActionResult Login(string returnUrl = "")
        {
            if (string.IsNullOrWhiteSpace(returnUrl))
            {
                returnUrl = Request.ApplicationPath;
            }

            return View(
                new LoginFormViewModel
                {
                    ReturnUrl = returnUrl,
                    IsMultiTenancyEnabled = _multiTenancyConfig.IsEnabled
                });
        }

        [HttpPost]
        public async Task<JsonResult> Login(LoginViewModel loginModel, string returnUrl = "", string returnUrlHash = "")
        {
            CheckModelState();

            var loginResult = await GetLoginResultAsync(
                loginModel.UsernameOrEmailAddress,
                loginModel.Password,
                loginModel.TenancyName
                );

            await SignInAsync(loginResult.User, loginResult.Identity, loginModel.RememberMe);

            if (string.IsNullOrWhiteSpace(returnUrl))
            {
                returnUrl = Request.ApplicationPath;
            }

            if (!string.IsNullOrWhiteSpace(returnUrlHash))
            {
                returnUrl = returnUrl + returnUrlHash;
            }

            return Json(new AjaxResponse { TargetUrl = returnUrl });
        }

        private async Task<AbpLoginResult<Tenant, User>> GetLoginResultAsync(string usernameOrEmailAddress, string password, string tenancyName)
        {
            try
            {

                var loginResult = await _logInManager.LoginAsync(usernameOrEmailAddress, password, tenancyName);

                switch (loginResult.Result)
                {
                    case AbpLoginResultType.Success:
                        return loginResult;
                    default:
                        throw CreateExceptionForFailedLoginAttempt(loginResult.Result, usernameOrEmailAddress, tenancyName);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }

        }

        private async Task SignInAsync(User user, ClaimsIdentity identity = null, bool rememberMe = false)
        {
            if (identity == null)
            {
                identity = await _userManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
            }

            AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
            AuthenticationManager.SignIn(new AuthenticationProperties { IsPersistent = rememberMe }, identity);
        }

        private Exception CreateExceptionForFailedLoginAttempt(AbpLoginResultType result, string usernameOrEmailAddress, string tenancyName)
        {
            switch (result)
            {
                case AbpLoginResultType.Success:
                    return new ApplicationException("Don't call this method with a success result!");
                case AbpLoginResultType.InvalidUserNameOrEmailAddress:
                case AbpLoginResultType.InvalidPassword:
                    return new UserFriendlyException(L("LoginFailed"), L("InvalidUserNameOrPassword"));
                case AbpLoginResultType.InvalidTenancyName:
                    return new UserFriendlyException(L("LoginFailed"), L("ThereIsNoTenantDefinedWithName{0}", tenancyName));
                case AbpLoginResultType.TenantIsNotActive:
                    return new UserFriendlyException(L("LoginFailed"), L("TenantIsNotActive", tenancyName));
                case AbpLoginResultType.UserIsNotActive:
                    return new UserFriendlyException(L("LoginFailed"), L("UserIsNotActiveAndCanNotLogin", usernameOrEmailAddress));
                case AbpLoginResultType.UserEmailIsNotConfirmed:
                    return new UserFriendlyException(L("LoginFailed"), "UserEmailIsNotConfirmedAndCanNotLogin");
                case AbpLoginResultType.LockedOut:
                    return new UserFriendlyException(L("LoginFailed"), L("UserLockedOutMessage"));
                default: //Can not fall to default actually. But other result types can be added in the future and we may forget to handle it
                    Logger.Warn("Unhandled login fail reason: " + result);
                    return new UserFriendlyException(L("LoginFailed"));
            }
        }

        public ActionResult Logout()
        {
            AuthenticationManager.SignOut();
            return RedirectToAction("Login");
        }

        #endregion

由於abp涉及到租戶和身份驗證的問題,所以登錄有點繁瑣.分析發現主要包括以下幾個步驟:

1、 GetLoginResultAsync --> loginManager.LoginAsync --> userManager.CreateIdentityAsync:不要以為調用了LoginAsync就以為是登錄,其實這是 偽登錄。主要根據用戶名密碼去核對用戶信息,構造User對象返回,然后再根據User對象的身份信息去構造身份證(CliamsIdentity)。
2、 SignInAsync --> AuthenticationManager.SignOut
-->AuthenticationManager.SignIn

AuthenticationManager(認證管理員),負責真正的登入登出。SignIn的時候將第一步構造的身份證(CliamsIdentity)交給證件所有者(ClaimsPrincipal)。   
       登錄完成之后,我們通常會有一個記住用戶名密碼的功能,有人就會想到abp中的AbpSession.單其實AbpSession不是單純意義上的Session,比如AbpSession里面的Userid就是通過以下方式獲得的.
((ClaimsPrincipal)Thread.CurrentPrincipal).Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);

       需要獲取會話信息則必須實現IAbpSession接口。雖然你可以用自己的方式去實現它(IAbpSession),但是它在module-zero項目中已經有了完整的實現。IAbpSession包含還有其他信息.

 //
    // 摘要:
    //     Defines some session information that can be useful for applications.
    public interface IAbpSession
    {
        //
        // 摘要:
        //     TenantId of the impersonator. This is filled if a user with Abp.Runtime.Session.IAbpSession.ImpersonatorUserId
        //     performing actions behalf of the Abp.Runtime.Session.IAbpSession.UserId.
        int? ImpersonatorTenantId { get; }
        //
        // 摘要:
        //     UserId of the impersonator. This is filled if a user is performing actions behalf
        //     of the Abp.Runtime.Session.IAbpSession.UserId.
        long? ImpersonatorUserId { get; }
        //
        // 摘要:
        //     Gets current multi-tenancy side.
        MultiTenancySides MultiTenancySide { get; }
        //
        // 摘要:
        //     Gets current TenantId or null. This TenantId should be the TenantId of the Abp.Runtime.Session.IAbpSession.UserId.
        //     It can be null if given Abp.Runtime.Session.IAbpSession.UserId is a host user
        //     or no user logged in.
        int? TenantId { get; }
        //
        // 摘要:
        //     Gets current UserId or null. It can be null if no user logged in.
        long? UserId { get; }

        //
        // 摘要:
        //     Used to change Abp.Runtime.Session.IAbpSession.TenantId and Abp.Runtime.Session.IAbpSession.UserId
        //     for a limited scope.
        //
        // 參數:
        //   tenantId:
        //
        //   userId:
        IDisposable Use(int? tenantId, long? userId);

             

AbpSession定義的一些關鍵屬性:

1.UserId: 當前用戶的標識ID,如果沒有當前用戶則為null.如果需要授權訪問則它不可能為空。

2.TenantId: 當前租戶的標識ID,如果沒有當前租戶則為null。

3.MultiTenancySide: 可能是Host或Tenant。

         UserId和TenantId是可以為null的。當然也提供了不為空時獲取數據的 GetUserId()和GetTenantId() 方法 。當你確定有當前用戶時,你可以使用GetUserId()方法。如果當前用戶為空,使用該方法則會拋出一個異常。GetTenantId()的使用方式和GetUserId()類似。

            IAbpSession通常是以屬性注入的方式存在於需要它的類中,不需要獲取會話信息的類中則不需要它。如果我們使用屬性注入方式,我們可以用 
NullAbpSession.Instance作為默認值來初始化它(IAbpSession)

    public IAbpSession AbpSession { get; set; }
        private readonly IUserService _iUsersService;
        public AccountController(IUserService iUsersService)
        {
            _iUsersService = iUsersService;
            AbpSession = NullAbpSession.Instance;
        }

        // GET: Account
        public ActionResult Index()
        {
            var currentUserId = AbpSession.UserId;
            return View(); 
        }

          由於授權是應用層的任務,因此我們應該在應用層和應用層的上一層使用IAbpSession(我們不在領域層使用IAbpSession是很正常的)。

ApplicationServiceAbpController 和 AbpApiController 這3個基類已經注入了AbpSession屬性,因此在Application Service的實例方法中,能直接使用AbpSession屬性。

           ABP框架中的AbpSession, 並沒有使用到System.Web.HttpSessionStateBase, 而是自己定義了一個Abp.Runtime.Session.IAbpSession接口, 並在Zero模塊中通過AspNet.Identity組件實現了AbpSession對象的存值、取值。 所以即使Web服務重啟,也不會丟失Session狀態。在我們自己的項目中, Session對象只有UserId、TenantId、MultiTenancySide這幾個屬性是不夠用的,可以自己擴充了幾個屬性和方法,使用起來非常方便。

      首先我們定義IAbpSession擴展類獲取擴展屬性,通過擴展類,我們不需要做其他額外的更改,即可通過ApplicationService, AbpController 和 AbpApiController 這3個基類已經注入的AbpSession屬性調用GetUserName()來獲取擴展的Name屬性。

 接口代碼:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace JCmsErp.AbpSessionExtension
{
   public interface IAbpSessionExtension
    {
        string UserName { get; }
    }
}

實現代碼:

using Abp.Configuration.Startup;
using Abp.MultiTenancy;
using Abp.Runtime;
using Abp.Runtime.Session;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;

namespace JCmsErp.AbpSessionExtension
{
    public class AbpSessionExtension : ClaimsAbpSession, IAbpSessionExtension
    {
        public AbpSessionExtension(IPrincipalAccessor principalAccessor, IMultiTenancyConfig multiTenancy, ITenantResolver tenantResolver, IAmbientScopeProvider<SessionOverride> sessionOverrideScopeProvider)
            : base(principalAccessor, multiTenancy, tenantResolver, sessionOverrideScopeProvider)
        {
        }

        public string UserName => GetUserName(ClaimTypes.Name);

        private string GetUserName(string claimType)
        {
            var claimsPrincipal = PrincipalAccessor.Principal;

            var claim = claimsPrincipal?.Claims.FirstOrDefault(c => c.Type == claimType);
            if (string.IsNullOrEmpty(claim?.Value))
                return null;

            return claim.Value;
        }


    }
}

 

       然后在登錄邏輯中加入以下代碼:

  //添加身份信息,以便在AbpSession中使用

            identity.AddClaim(new Claim(ClaimTypes.Name, user.UserName));

就這樣,我們在ApplicationService, AbpController 和 AbpApiController任何地方注入IAbpSession,然后AbpSession.Name就能獲取到我們登錄時候添加的信息.

       二,abp的錯誤機制

          如果登錄過程中出錯怎么辦,報錯了ABP怎么反應,我們來看一下abp的錯誤機制.在web應用中,異常通常在MVC Controller actions和Web API Controller actions中處理。當異常發生時,應用程序的用戶以某種方式被告知錯誤的相關信息及原因。果錯誤在正常的HTTP請求時發生,將會顯示一個異常頁。如果在AJAX請求中發生錯誤,服務器發送錯誤信息到客戶端,然后客戶端處理錯誤並顯示給用戶。在所有的Web請求中處理異常是件乏味且重復的工作。ABP自動化完成異常處理,幾乎從不需要顯示的處理任何異常。ABP處理所有的異常、記錄異常並返回合適、格式化的響應到客戶端。在客戶端處理這些響應並將錯誤信息顯示給用戶。

     異常顯示,首先我們在ActionResult 隨便添加一個異常信息,調試一下看一下結果

   

   public ActionResult Index()
        {

           // return View();
             throw new Exception("登錄密碼錯誤或用戶不存在或用戶被禁用。");
        }

         當然,這個異常可能由另一個方法拋出,而這個方法的調用在這個action里。ABP處理這個異常、記錄它並顯示'Error.cshtml'視圖。你可以自定義這個視圖來顯示錯誤。一個示例錯誤視圖(在ABP模板中的默認錯誤視圖):

 

 

BP對用戶隱藏了異常的細節並顯示了一個標准(本地化的)的錯誤信息,除非你顯示的拋出一個UserFriendlyException,UserFriendlyException UserFriendlyException是一個特殊類型的異常,它直接顯示給用戶。參見下面的示例:

    // GET: Account
        public ActionResult Index()
        {

           // return View();
           throw new Abp.UI.UserFriendlyException("登錄密碼錯誤或用戶不存在或用戶被禁用。");
        }

瀏覽器結果:

 

 

 

所以,如果你想顯示一個特定的錯誤信息給用戶,那就拋出一個UserFriedlyException(或者一個繼承自這個類的異常)。

      當然如果是ajax請求里面出錯,message API處理JSON對象並顯示錯誤信息給用戶。前端應該有相應的錯誤處理.

  返回總目錄:ABP+AdminLTE+Bootstrap Table權限管理系統一期


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM