ABP+AdminLTE+Bootstrap Table權(quán)限管理系統(tǒng)一期
Github:https://github.com/Jimmey-Jiang/ABP-ASP.NET-Boilerplate-Project-CMS
前往博客園總目錄:ABP+AdminLTE+Bootstrap Table權(quán)限管理系統(tǒng)一期
上一節(jié)我們講到登錄邏輯,我做的登錄邏輯很簡單的,我們來看一下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涉及到租戶和身份驗證的問題,所以登錄有點繁瑣.分析發(fā)現(xiàn)主要包括以下幾個步驟:
-
GetLoginResultAsync-->loginManager.LoginAsync-->userManager.CreateIdentityAsync:不要以為調(diào)用了LoginAsync就以為是登錄,其實這是偽登錄。主要根據(jù)用戶名密碼去核對用戶信息,構(gòu)造User對象返回,然后再根據(jù)User對象的身份信息去構(gòu)造身份證(CliamsIdentity)。 -
SignInAsync-->AuthenticationManager.SignOut-->AuthenticationManager.SignIn:AuthenticationManager(認(rèn)證管理員),負(fù)責(zé)真正的登入登出。SignIn的時候?qū)⒌谝徊綐?gòu)造的身份證(CliamsIdentity)交給證件所有者(ClaimsPrincipal)。
登錄完成之后,我們通常會有一個記住用戶名密碼的功能,有人就會想到abp中的AbpSession.單其實AbpSession不是單純意義上的Session,比如AbpSession里面的Userid就是通過以下方式獲得的.
((ClaimsPrincipal)Thread.CurrentPrincipal).Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
需要獲取會話信息則必須實現(xiàn)IAbpSession接口。雖然你可以用自己的方式去實現(xiàn)它(IAbpSession),但是它在module-zero項目中已經(jīng)有了完整的實現(xiàn)。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.
//
// 參數(shù):
// tenantId:
//
// userId:
IDisposable Use(int? tenantId, long? userId);
AbpSession定義的一些關(guān)鍵屬性:
-
UserId: 當(dāng)前用戶的標(biāo)識ID,如果沒有當(dāng)前用戶則為null.如果需要授權(quán)訪問則它不可能為空。 -
TenantId: 當(dāng)前租戶的標(biāo)識ID,如果沒有當(dāng)前租戶則為null。 -
MultiTenancySide: 可能是Host或Tenant。
UserId和TenantId是可以為null的。當(dāng)然也提供了不為空時獲取數(shù)據(jù)的GetUserId()和GetTenantId()方法 。當(dāng)你確定有當(dāng)前用戶時,你可以使用GetUserId()方法。如果當(dāng)前用戶為空,使用該方法則會拋出一個異常。GetTenantId()的使用方式和GetUserId()類似。
IAbpSession通常是以屬性注入的方式存在于需要它的類中,不需要獲取會話信息的類中則不需要它。如果我們使用屬性注入方式,我們可以用 NullAbpSession.Instance作為默認(rèn)值來初始化它(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();
}
由于授權(quán)是應(yīng)用層的任務(wù),因此我們應(yīng)該在應(yīng)用層和應(yīng)用層的上一層使用IAbpSession(我們不在領(lǐng)域?qū)邮褂?code>IAbpSession是很正常的)。
ApplicationService, AbpController和 AbpApiController這3個基類已經(jīng)注入了AbpSession屬性,因此在Application Service的實例方法中,能直接使用AbpSession屬性。
ABP框架中的AbpSession, 并沒有使用到System.Web.HttpSessionStateBase, 而是自己定義了一個Abp.Runtime.Session.IAbpSession接口, 并在Zero模塊中通過AspNet.Identity組件實現(xiàn)了AbpSession對象的存值、取值。 所以即使Web服務(wù)重啟,也不會丟失Session狀態(tài)。在我們自己的項目中,Session對象只有UserId、TenantId、MultiTenancySide這幾個屬性是不夠用的,可以自己擴(kuò)充了幾個屬性和方法,使用起來非常方便。

首先我們定義
IAbpSession擴(kuò)展類獲取擴(kuò)展屬性,通過擴(kuò)展類,我們不需要做其他額外的更改,即可通過ApplicationService, AbpController和AbpApiController 這3個基類已經(jīng)注入的AbpSession屬性調(diào)用
GetUserName()來獲取擴(kuò)展的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; }
}
}
實現(xiàn)代碼:
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的錯誤機(jī)制
如果登錄過程中出錯怎么辦,報錯了ABP怎么反應(yīng),我們來看一下abp的錯誤機(jī)制.在web應(yīng)用中,異常通常在
MVC Controller actions和Web API Controller actions中處理。當(dāng)異常發(fā)生時,應(yīng)用程序的用戶以某種方式被告知錯誤的相關(guān)信息及原因。果錯誤在正常的HTTP請求時發(fā)生,將會顯示一個異常頁。如果在AJAX請求中發(fā)生錯誤,服務(wù)器發(fā)送錯誤信息到客戶端,然后客戶端處理錯誤并顯示給用戶。在所有的Web請求中處理異常是件乏味且重復(fù)的工作。ABP自動化完成異常處理,幾乎從不需要顯示的處理任何異常。ABP處理所有的異常、記錄異常并返回合適、格式化的響應(yīng)到客戶端。在客戶端處理這些響應(yīng)并將錯誤信息顯示給用戶。異常顯示,首先我們在
ActionResult隨便添加一個異常信息,調(diào)試一下看一下結(jié)果
//添加身份信息,以便在AbpSession中使用
identity.AddClaim(new Claim(ClaimTypes.Name, user.UserName));
當(dāng)然,這個異??赡苡闪硪粋€方法拋出,而這個方法的調(diào)用在這個action里。ABP處理這個異常、記錄它并顯示Error.cshtml視圖。你可以自定義這個視圖來顯示錯誤。一個示例錯誤視圖(在ABP模板中的默認(rèn)錯誤視圖):

BP對用戶隱藏了異常的細(xì)節(jié)并顯示了一個標(biāo)準(zhǔn)(本地化的)的錯誤信息,除非你顯示的拋出一個UserFriendlyException,UserFriendlyException UserFriendlyException是一個特殊類型的異常,它直接顯示給用戶。參見下面的示例:
// GET: Account
public ActionResult Index()
{
// return View();
throw new Abp.UI.UserFriendlyException("登錄密碼錯誤或用戶不存在或用戶被禁用。");
}
瀏覽器結(jié)果:

所以,如果你想顯示一個特定的錯誤信息給用戶,那就拋出一個UserFriedlyException(或者一個繼承自這個類的異常)。
當(dāng)然如果是ajax請求里面出錯,message API處理JSON對象并顯示錯誤信息給用戶。前端應(yīng)該有相應(yīng)的錯誤處理.
返回簡書總目錄:ABP+AdminLTE+Bootstrap Table權(quán)限管理系統(tǒng)一期
前往博客園總目錄:ABP+AdminLTE+Bootstrap Table權(quán)限管理系統(tǒng)一期