大家好,我是張飛洪,感謝您的閱讀,我會不定期和你分享學習心得,希望我的文章能成爲你成長路上的墊腳石,讓我們一起精進。
授權、驗證、異常處理和日誌記錄等橫切關注點是每個系統的基本組成部分,它們對於確保系統的安全和良好運行至關重要。
實現橫切關注點會導致應用中的很多地方出現重複代碼。此外,一次授權或驗證檢查缺失可能會導致整個系統崩潰。
ABP框架的主要目標之一是使你的應用“不要重複自己”(DRY),ASP.NET Core已經爲一些跨領域的問題提供了一個良好的基礎設施,但ABP進一步實現了自動化,讓使用更加容易。
本章探討了ABP的基礎設施:
- 認證授權
- 用戶驗證
- 異常處理
認證和授權是安全中的兩個主要概念。身份驗證是識別當前用戶的過程,授權用於允許或禁止用戶執行應用的特定操作。
ASP.NET Core
系統本身提供了一種高級而靈活的認證和授權,ABP框架的認證授權與ASP.NET Core
100%兼容,並進行了一定的擴展,它允許將權限授予角色和用戶,它還允許在客戶端進行權限檢查。
簡單授權檢查
最簡單的場景,只允許登錄的用戶執行特定操作。
[Authorize]
屬性不帶任何參數,只檢查當前用戶是否已通過身份驗證(登錄)。
請參見以下控制器(MVC):
public class ProductController : Controller {
public async Task GetListAsync(){}
[Authorize]
public async Task CreateAsync(ProductCreationDto input){}
[Authorize]
public async Task DeleteAsync(Guid id){}
}
在本例中,CreateAsync
和DeleteAsync
操作僅允許通過身份驗證的用戶使用,假設匿名用戶(尚未登錄的用戶)嘗試執行這些操作,ASP.NET Core
向客戶端返回授權錯誤響應。而GetListAsync
方法對每個人都可用,甚至對匿名用戶也是如此。
Authorize
可在Controller
級別,用於授權內部的所有Actions
操作。如果想允許匿名用戶執行特定操作,可以配置[AllowAnonymous]
屬性。如以下代碼塊所示:
[Authorize]
public class ProductController : Controller {
[AllowAnonymous]
public async Task> GetListAsync(){}
public async Task CreateAsync(ProductCreationDto input) {}
public async Task DeleteAsync(Guid id){}
}
在這裏,我在類ProductController
的頂部使用了[Authorize]
屬性,在GetListAsync
方法使用[AllowAnonymous]
屬性,這使得尚未登錄的用戶也可以訪問GetListAsync
方法。
雖然無參數的[Authorize]
屬性有一些適用場景,但是如果我們想要定義特定的權限(或策略),使得所有經過身份驗證的用戶具有不同的權限。
權限系統
ABP框架對
ASP.NET Core
最重要的擴展是權限系統。權限是爲特定用戶或角色授予或禁止的策略,它與應用功能進行關聯,並在用戶嘗試使用該功能時進行檢查。如果當前用戶已被授予權限,則該用戶可以使用功能。否則,用戶無法使用該功能。
ABP提供了在應用中定義、授予和檢查權限的功能。
1 定義權限
在使用權限之前需要先定義權限,首先創建從PermissionDefinitionProvider
類繼承的類。創建新的ABP解決方案時,會有一個空的權限定義提供程序類(在Application.Contracts
項目中)。請參見以下示例:
public class ProductManagementPermissionDefinitionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
var myGroup = context.AddGroup("ProductManagement");
myGroup.AddPermission("ProductManagement.ProductCreation");
myGroup.AddPermission"ProductManagement.ProductDeletion");
}
}
ABP框架在應用啓動時調用Define
方法。在本例中,我創建了一個名爲ProductManagement
的權限組,並在其中定義了兩個權限,用於對用戶界面(UI)上的權限進行分組,通常每個模塊都要定義其權限組。組和權限名稱是任意string
字符串值(建議定義const
常量字段)。
這是一個最小的配置,您還可以將顯示名稱指定本地化字符串,並指定權限名稱,以便在UI上以用戶友好的方式顯示它們。以下代碼塊使用本地化系統指定顯示名稱,同時定義組和權限:
public class ProductManagementPermissionDefinitionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
var myGroup = context.AddGroup("ProductManagement",L("ProductManagement"));
myGroup.AddPermission("ProductManagement.ProductCreation",L("ProductCreation"));
myGroup.AddPermission("ProductManagement.ProductDeletion",L("ProductDeletion"));
}
private static LocalizableString L(string name)
{
return LocalizableString.Create(name);
}
}
我定義了一個L
方法來簡化本地化。(第8章“使用ABP的功能和服務”中將詳細介紹本地化系統)
多租戶中的權限定義
對於多租戶應用程序,可以爲AddPermission
方法指定multiTenancySide
參數,以定義僅限主機或僅限租戶的權限。(第16章“實現多租戶”中將詳細介紹多租戶)。
定義完權限後,下一次應用啓動後,該權限就可以使用了(在“權限管理”對話框中)。
2 管理權限界面
默認情況下,可以爲用戶或角色授予權限。假設您創建了一個經理角色(manager),並希望爲該角色授予產品權限。程序啓動後,我們導航到管理|身份管理|角色頁面。然後創建經理角色(如果之前沒有創建),請單擊權限操作按鈕,如圖所示
角色管理頁面
單擊權限按鈕後將打開一個對話框,如下所示:
在圖中,您可以在左側看到權限組,而該組中的權限在右側可用。權限組和我們定義的權限已經可以使用,無需進行任何額外操作。
具有經理角色的用戶都繼承該角色的權限。用戶可以有多個角色,並且繼承所有分配角色的所有權限的聯合。您還可以在“用戶管理”頁面上直接向用戶授予權限,以獲得更大的靈活性。
我們已經定義了權限並將其分配給了角色。下一步是檢查當前用戶是否具有請求的權限。
3 檢查權限
3.1[Authorize]
屬性
您可以使用[Authorize]
屬性以聲明的方式檢查權限,也可以使用IAuthorizationService
以編程方式檢查權限。
我們可以重寫上面的ProductController
類,以授予產品創建和刪除權限,如下所示:
public class ProductController : Controller
{
public async Task<List<ProductDto>> GetListAsync(){}
[Authorize("ProductManagement.ProductCreation")]
public async Task CreateAsync(ProductCreationDto input){}
[Authorize("ProductManagement.ProductDeletion")]
public async Task DeleteAsync(Guid id){}
}
[Authorize]
屬性將字符串參數作爲策略名稱。ABP將權限定義爲自動策略,您可以在需要指定策略名稱的任何位置使用權限名稱。
3.2 IAuthorizationService
聲明式授權易於使用,建議儘可能使用。但是,當您想要有條件地檢查權限或執行未授權案例的邏輯時,它是有限的。對於這種情況,可以注入並使用IAuthorizationService
,如下例所示
public class ProductController : Controller
{
private readonly IAuthorizationService _authorizationService;
public ProductController(IAuthorizationService authorizationService)
{
_authorizationService = authorizationService;
}
public async Task CreateAsync(ProductCreationDto input)
{
if (await _authorizationService.IsGrantedAsync("ProductManagement.ProductCreation"))
{
// TODO: Create the product
}
else
{
// TODO: Handle unauthorized case
}
}
}
IsGrantedAsync
方法檢查給定的權限,如果當前用戶(或用戶的角色)已被授予權限,則返回true
。如果您有自定義邏輯的權限要求,這將非常有用。但是,如果您只想檢查權限並對未經授權的情況拋出異常,CheckAsync
方法更實用:
public async Task CreateAsync(ProductCreationDto input)
{
await _authorizationService.CheckAsync("ProductManagement.ProductCreation");
//TODO: Create the product
}
如果用戶沒有該操作的權限,CheckAsync
方法會引發AbpAuthorizationException
異常,該異常由ABP框架處理,並向客戶端返回HTTP響應。IsGrantedAsync
和CheckAsync
方法是ABP框架定義的有用的擴展方法。
[warning] 提示:從
AbpController
繼承
建議從AbpController
類而不是標準Controller
類派生。因爲它內部做了擴展,定義了一些有用的屬性。比如,它有AuthorizationService
屬性(屬於IAuthorizationService
類型),您可以直接使用它,無需手動注入IAuthorizationService
接口。
服務器上的權限檢查是一種常見的方法。但是,您可能還需要檢查客戶端的權限。
4 客戶端權限
ABP公開了一個標準HTTP API,其URL爲/api/abp/application-configuration
,返回包含本地化文本、設置、權限等的JSON數據。客戶端可以使用該API來檢查權限或在客戶端執行本地化。
不同的客戶端類型可能會提供不同的服務來檢查權限。例如,在MVC/Razor Pages
中,可以使用abp.auth
JavaScript API檢查權限,如下所示:
abp.auth.isGranted('ProductManagement.ProductCreation');
這是一個全局函數,如果當前用戶具有給定的權限,則返回true
。否則,返回false
。
在Blazor應用程序中,可以重用相同的[Authorize]
屬性和IAuthorizationService
。
我們將在第4部分“用戶界面和API開發”中詳細介紹客戶端權限檢查。
5 子權限
在複雜的應用中,可能需要創建一些依賴於其父權限的子權限。當父權限被授予時,子權限才能正常工作。
角色管理權限具有一些子權限,如創建、編輯和刪除。角色管理權限用於授權用戶進入角色管理頁面。如果用戶無法進入該頁面,那麼授予角色創建權限就沒有意義,因爲不進入該頁面幾乎不可能創建新角色。
在權限定義類中,AddPermission
方法返回創建的權限,並將其分配給變量,變量使用AddChild
方法創建子權限,如下代碼塊所示
public override void Define(IpermissionDefinitionContext context)
{
var myGroup = context.AddGroup("ProductManagement",L("ProductManagement"));
var parent = myGroup.AddPermission("MyParentPermission");
parent.AddChild("MyChildPermission");
}
在本例,我們創建了一個名爲MyParentPermission
的父權限,然後創建了另一個名爲MyChildPermission
的子權限。
子權限也可以具有子權限,比如我們可以把parent.AddChild
的返回值賦予一個變量,然後調用它AddChild
方法繼續添加子權限。
通過開/關策略授權來定義和使用權限,顯得簡單而強大,然而,ASP.NET Core允許創建完整的自定義邏輯來定義策略。
基於策略的授權
ASP.NET Core基於策略的授權機制允許您授權應用中的某些操作,就像使用權限一樣。但這一次,使用代碼表示的自定義邏輯,實際上是ABP框架提供的一種簡單且自動化的策略。
定義權限需求
首先需要定義一個創建產品的權限需求(我們可以在應用層中定義這些類),稍後檢查,代碼段:
public class ProductCreationRequirement : IAuthorizationRequirement { }
ProductCreationRequirement
是一個空類,僅實現IAuthorizationRequirement
接口。然後,爲該需求定義一個授權處理程序ProductCreationRequirementHandler
,如下所示:
public class ProductCreationRequirementHandler : AuthorizationHandler<ProductCreationRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,ProductCreationRequirement requirement)
{
if (context.User.HasClaim(c => c.Type == "productManager"))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
處理程序必須派生自AuthorizationHandler<T>
,其中T
是ProductCreationRequirement
類型。在本例中,我只是檢查了當前用戶是否擁有productManager
聲明,這是我的自定義聲明(聲明是存儲在身份驗證票據中的值)。您可以構建自定義邏輯。如果允許當前用戶擁有創建產品需求,你要做的就是調用context.Succeed
上下文。
定義權限需求和處理程序後,需要在模塊類的ConfigureServices
方法中註冊它們,如下所示:
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AuthorizationOptions>(options =>
{
options.AddPolicy("ProductManagement.ProductCreation",
policy => policy.Requirements.Add(new ProductCreationRequirement()));
});
context.Services.AddSingleton<IAuthorizationHandler,ProductCreationRequirementHandler>();
}
我使用AuthorizationOptions
定義了一個名爲ProductManagement.ProductCreation
的策略。然後,我將ProductCreationRequirementHandler
註冊爲單例服務。
現在,假設我對Controller
或Action
使用[Authorize("ProductManagement.ProductCreation")]
屬性,或者使用IAuthorizationService
檢查策略,我的自定義授權處理程序就可以進行授權邏輯處理了。
權限與自定義策略
一旦實現了自定義策略,就不能使用“權限管理”對話框向用戶和角色授予權限,因爲它不是一個簡單的啓用/禁用權限。然而,客戶端策略檢查仍然有效,因爲ABP很好地集成到ASP.NET Core的政策體系。
如果您只需要開/關方式的策略,ABP的權限系統很容易很強大,而自定義策略允許您使用自定義邏輯動態檢查策略。
基於資源的授權
ASP.NET Core的授權系統比本文介紹的功能更多。基於資源的授權是一種允許您基於對象(如實體)控制策略的功能。例如,您可以控制刪除特定產品的訪問權限,而不是對所有產品擁有共同的刪除權限。ABP與ASP.NET Core完全兼容。建議你查看ASP.NET Core的文檔,以瞭解有關授權的更多信息。
到目前爲止,我們已經在MVC控制器上看到了[Authorize]
屬性的用法。但是,此屬性和IAuthorizationService
不限於控制器。
控制器之外的授權
ASP.NET Core允許您對Razor頁面、Razor組件和Web層中的一些地方使用[Authorize]
和IAuthorizationService
。
ABP框架更進一步,允許對服務類和方法使用[Authorize]
屬性,而不依賴於Web層,即使在非Web應用程序中也是如此。因此,這種用法完全有效,如下所示:
public class ProductAppService : ApplicationService, IProductAppService
{
[Authorize("ProductManagement.ProductCreation")]
public Task CreateAsync(ProductCreationDto input)
{
// TODO
}
}
只有當前用戶擁有ProductManagement.ProductCreation
(產品創建)權限/策略時,才能執行CreateAsync
方法。實際上,[Authorize]
在任何註冊爲依賴注入(DI)的類中都是可用的。然而,由於授權被認爲是應用層的一個功能,因此建議在應用層而不是領域層使用授權。
動態代理/攔截器
ABP使用使用攔截器的動態代理來完成方法調用的授權檢查。如果通過類引用(而不是接口引用)注入服務,動態代理系統將使用動態繼承技術。在這種情況下,必須使用virtual
關鍵字定義方法,以允許動態代理系統覆蓋它並執行授權檢查。
驗證類別
驗證可確保數據的安全性和一致性,並幫助應用程序正常運行。驗證話題很廣,有一些常見的驗證類別:
- 客戶端驗證:用於在將數據發送到服務器之前預先驗證用戶輸入。這對用戶體驗(UX)很重要,您應該儘可能地實現它。例如,檢查所需的文本框字段是否爲空是一種客戶端驗證。(我們將在第4部分“用戶界面和API開發”中介紹客戶端驗證)
- 服務器端驗證:由服務器執行,以防止不完整、格式錯誤或惡意請求。它爲應用程序提供一定程度的安全性。例如,檢查服務器端的必填輸入字段是否爲空就是此類驗證的一個例子。
- 業務驗證:也在服務器中執行,用於驗證業務規則,並保證業務數據的一致性。它在業務代碼的每一個級別都可以執行,例如,在轉賬之前檢查用戶的餘額是一種業務驗證。
關於ASP.NET Core
的驗證系統:
ASP.NET Core
爲驗證提供了許多選項。本書重點介紹ABP框架添加的功能。
本節重點介紹服務端驗證,以及驗證過程和驗證異常處理的方法。
讓我們從最簡單的數據註釋特性驗證開始:
註釋驗證(Data annotation attributes)
public class ProductAppService : ApplicationService, IProductAppService
{
public Task CreateAsync(ProductCreationDto input)
{
// TODO
}
}
public class ProductCreationDto {
[Required]
[StringLength(100)]
public string Name { get; set; }
[Range(0, 999.99)]
public decimal Price { get; set; }
[Url]
public string PictureUrl { get; set; }
public bool IsDraft { get; set; }
}
ProductAppService
是應用服務,它的入參ProductCreationDto
在ABP框架中自動驗證,就像ASP.NET Core MVC
框架一樣。
ProductCreationDto
有三個驗證屬性,採用的是ASP.NET Core
有內置的驗證屬性,此外ASP.NET Core
還有其他內置驗證屬性:
[Required]
: 非空驗證[StringLength]
: 字符串長度大小驗證[Range]
: 範圍驗證[Url]
: Url格式驗證[RegularExpression]
: 正則表達式(regex)驗證[EmailAddress]
: 電子郵件驗證
ASP.NET Core
還允許您通過繼承ValidationAttribute
類並重寫IsValid
方法來自定義驗證。
註釋驗證簡單易用,推薦在DTO和模型上使用。但不適用自定義邏輯驗證(會受到限制)
使用接口 IValidatableObject
自定義驗證
模型或DTO對象可以實現 IValidatableObject
接口,實現自定義代碼塊驗證。請參見以下示例:
public class ProductCreationDto : IValidatableObject
{
...
[Url]
public string PictureUrl { get; set; }
public bool IsDraft { get; set; }
public IEnumerable Validate(ValidationContext context)
{
if (IsDraft == false && string.IsNullOrEmpty(PictureUrl))
{
yield return new ValidationResult("Picture must be provided to publish a product",new []{ nameof(PictureUrl) });
}
}
}
在本例中,ProductCreationDto
有一個自定義規則:如果IsDraft
爲false
,並且圖片路徑爲控,則提示需要上傳圖片。
如果需要從DI系統解析服務,可以使用context.GetRequiredService
方法。例如,如果我們想本地化錯誤消息,我們可以重寫Validate
方法,如下代碼塊所示:
public IEnumerable Validate(ValidationContext context)
{
if (IsDraft == false && string.IsNullOrEmpty(PictureUrl))
{
var localizer = context.GetRequiredService<IStringLocalizer<ProductManagementResource>();
yield return new ValidationResult(localizer["PictureIsMissingErrorMessage"],new []{ nameof(PictureUrl) });
}
}
這裏,我們從DI解析IStringLocalizer<ProductManagementResource>
實例,並用它向客戶端返回本地化錯誤消息。(我們將在第8章詳細介紹本地化系統)
正式驗證與業務驗證
作爲最佳實踐,只在DTO/Model類中實現正式驗證。然而,在應用或領域層服務中的業務邏輯驗證,例如,檢查數據庫中是否已經存在給定的產品名稱,則不要在Validate
方法中驗證。
驗證異常
1 自動異常
如果用戶輸入無效,ABP框架會自動拋出AbpValidationException
類型的異常。以下情況會引發異常:
- 輸入對象爲null,因此不需要檢查它是否爲null。
- 輸入對象總是無效的,所以您不必在API控制器中檢查
Model.IsValid
。
在這些情況下,ABP不會調用您的服務方法(或Controller Action)。要想正確執行,必須確保輸入不爲null
而且有效。
2 手動異常
如果在服務內部執行其他驗證,並希望引發與驗證相關的異常,還可以引發AbpValidationException
,如以下代碼段所示:
public async Task CreateAsync(ProductCreationDto input) {
if (await HasExistingProductAsync(input.Name)){
throw new AbpValidationException(new List<ValidationResult>{new ValidationResult("Product name is already in use!", new[] {nameof(input.Name)})});
}
}
這裏,我們假設HasExistingProductAsync
在存在產品時返回true
。我們通過指定驗證錯誤來拋出AbpValidationException
。ValidationResult
表示驗證錯誤;它的第一個構造函數參數是驗證錯誤消息,第二個參數(可選)是DTO屬性的名稱。
一旦您或ABP驗證系統拋出AbpValidationException
異常,ABP異常處理系統將捕獲並處理它。
禁用驗證
可以使用[DisableValidation]
在方法或類級別繞過ABP驗證系統,如下例所示:
[DisableValidation]
public async Task CreateAsync(ProductCreationDto input) { }
在本例中,CreateAsync
方法用[DisableValidation]
修飾,因此ABP不會對輸入對象執行任何自動驗證。
如果對類使用[DisableValidation]
,則該類的所有方法的驗證都將被禁用。在這種情況下,可以對某個方法使用[EnableValidation]
,以便僅對該特定方法啓用驗證。
當禁用方法的自動驗證時,仍然可以執行自定義驗證邏輯並拋出AbpValidationException
,如前一節所述。
其他類型的驗證
除了對Controller Actions
和Razor Page handlers
執行驗證,ABP還允許爲應用中的任何類啓用自動驗證功能。您只需實現IValidationEnabled
接口,如下例所示:
public class SomeServiceWithValidation : IValidationEnabled, ITransientDependency { ... }
然後,ABP使用本章介紹的驗證系統自動驗證所有輸入。
動態代理/攔截器
ABP使用使用攔截器的動態代理來完成方法調用的驗證。如果通過類引用(而不是接口引用)注入服務,動態代理系統將使用動態繼承技術。在這種情況下,必須使用virtual
關鍵字定義方法,以允許動態代理系統覆蓋它並執行驗證。
到目前爲止,我們已經介紹了與ASP.NET Core兼容的ABP驗證系統。最後我們將介紹FluentValidation
庫集成,它允許您將驗證邏輯與驗證對象分離。
整合FluentValidation庫
大多數情況,內置的驗證系統就足夠了,而且它很容易定義驗證規則,我個人認爲它沒有任何問題,在DTO/model類中嵌入數據驗證邏輯是完全可行的。然而,一些開發人員認爲DTO/model類內部嵌入驗證邏輯是一種糟糕的做法。在這種情況下,ABP提供了一個與流行的FluentValidation
庫的集成包,它將驗證邏輯與DTO/model類分離,並提供了比標準註釋驗證方法更強大的功能。
要使用FluentValidation
庫,首先需要將其安裝到項目中。可以使用ABP命令行界面(ABP CLI)的add-package
命令爲項目安裝它,如下所示:
abp add-package Volo.Abp.FluentValidation
安裝完軟件包後,可以創建驗證類並設置驗證規則,如下代碼塊所示:
public class ProductCreationDtoValidator : AbstractValidator
{
public ProductCreationDtoValidator()
{
RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
RuleFor(x => x.Price).ExclusiveBetween(0, 1000);
//...
}
}
具體請參閱FluentValidation文檔,瞭解如何定義更高級的驗證規則:.
ABP自動發現驗證類,並將它們集成到驗證過程中。這意味着您甚至可以將標準驗證邏輯與FluentValidation
驗證類混合使用。
一個系統最重要的質量指標之一是:它如何響應錯誤和異常情況。它應該積極處理錯誤,並向客戶端返回正確的響應,並優雅地將問題告知用戶。
在Web開發中,如果每個客戶端請求異常都要處理一遍,對開發人員來說就顯得重複而繁瑣。
ABP框架完全自動化了程序中各方面的錯誤處理。大多數情況下,您無需在代碼中編寫任何try-catch
語句,因爲它會執行以下操作:
- 處理、記錄所有異常,並向客戶端返回標準格式的錯誤信息,或爲服務渲染提供標準錯誤頁面。
- 隱藏內部結構性錯誤,同時支持返回用戶友好的本地化錯誤消息。
- 支持標準異常,例如驗證和授權異常,並向客戶端發送正確的HTTP狀態碼。
- 處理客戶端上的錯誤,並向用戶顯示有意義的消息。
當ABP異常系統支持向客戶端返回用戶友好的消息或特定錯誤代碼(業務)。
用戶友好異常 UserFriendlyException
ABP提供了一些預定義的異常類來定製錯誤處理行爲。其中之一是UserFriendlyException
類。
首先,要了解UserFriendlyException
使用場景,先要了解服務端API是什麼異常。以下是自定義異常範例:
Public async Task ExampleAsync() { throw new Exception("my error message..."); }
假設瀏覽器客戶端通過AJAX請求ExampleAsync
方法。它將向用戶顯示以下錯誤消息:
如圖所示,ABP顯示了內部異常的標準消息,實際的錯誤消息會寫入日誌系統。對於此類一般性錯誤,服務器會向客戶端返回HTTP 500狀態代碼,因爲向用戶顯示原始異常消息是沒有用的,甚至可能是危險的,因爲它可能包含內部系統的一些敏感信息,例如數據庫表名和字段。
但是,對於某些特定情況,您可能希望向用戶返回一條用戶友好、信息豐富的自定義錯誤消息。對於這種情況,可以使用UserFriendlyException
異常,如下代碼塊所示:
public async Task ExampleAsync() { throw new UserFriendlyException("This message is available to the user!"); }
此時,ABP不會隱藏錯誤消息:
UserFriendlyException
不是唯一的,任何繼承自UserFriendlyException
或實現IUserFriendlyException
接口的異常類都可返回用戶友好的異常消息。
當您拋出用戶友好的異常時,ABP會向客戶端返回HTTP 403(禁止)狀態碼。(有關HTTP狀態碼映射,請參閱末尾的“控制HTTP狀態碼”部分)
[success]
UserFriendlyException
是一種特殊類型的業務異常,您可以直接向用戶返回消息。
業務異常 BusinessException
當請求的操作不滿足系統業務些規則時,需要拋出異常。ABP中的業務異常是ABP框架識別和處理的特殊異常類型。
在最簡單的情況下,可以直接使用BusinessException
類拋出業務異常。請參見EventHub
項目示例
public class EventRegistrationManager : DomainService
{
public async Task RegisterAsync(Event @event, AppUser user)
{
if (Clock.Now > @event.EndTime)
{
throw new BusinessException(EventHubErrorCodes.CantRegisterOrUnregisterForAPastEvent);
}
...
}
}
EventRegistrationManager
是一個領域服務,用於執行事件註冊的業務規則。RegisterAsync
是檢查事件時間,如果是註冊到過去的事件則引發業務異常。
BusinessException
的構造函數接受幾個參數,所有參數都是可選的:
code
: 自定義錯誤碼。客戶端可以在處理異常時進行檢查、跟蹤錯誤類型。不同的異常,通常使用不同的錯誤碼。錯誤碼還支持本地化。message
: 異常消息details
: 詳細消息innerException
: 內部異常。如果緩存了一個業務異常,則可以傳遞到這裏。logLevel
: 異常日誌級別,它是LogLevel
類型的枚舉,默認值是LogLevel.Warning
1 本地化業務異常
如果使用UserFriendlyException
,則必須自己對消息進行本地化,因爲異常消息將要顯示給用戶。
如果拋出BusinessException
,ABP不會向用戶顯示異常消息,除非顯式地將其本地化。爲此,它使用了錯誤代碼名稱空間。
假設您使用了EventHub:CantRegisterOrUnregisterForAPastEvent
作爲錯誤代碼。這裏,EventHub
通過使用冒號成爲錯誤代碼命名空間。我們必須將錯誤代碼名稱空間映射到本地化資源,這樣ABP就可以知道這些錯誤消息使用哪個本地化資源:
Configure(options => { options.MapCodeNamespace("EventHub",typeof(EventHubResource)); });
在這個代碼片段中,我們將EventHub
錯誤代碼命名空間映射到EventHubResource
本地化資源。現在,您可以在本地化文件(包括名稱空間)中將錯誤代碼定義爲key
,如下所示:
{"culture": "en", "texts": { "EventHub:CantRegisterOrUnregisterForAPastEvent": "You can not register to or unregister from an event in the past, sorry!" } }
配置完成後,每當您拋出帶有該錯誤代碼的BusinessException
異常時,ABP都會向用戶顯示本地化消息。
在某些情況下,您可能希望在錯誤消息中包含一些附加數據。請參閱以下代碼片段:
throw new BusinessException(EventHubErrorCodes.OrganizationNameAlreadyExists).WithData("Name", name);
在這裏,我們使用WithData
擴展方法將組織名稱包含在錯誤消息中。然後,我們可以定義本地化字符串,如以下代碼段所示:
"EventHub:OrganizationNameAlreadyExists": "The organization {Name} already exists. Please use another name."
在本例中,{Name}
是組織名稱的佔位符。ABP會自動將其替換爲給定的名稱。
我們已經看到了如何拋出BusinessException
異常。如果要創建自定義異常類呢?
2 自定義業務異常類
還可以創建自定義異常類,而不是直接引發BusinessException
異常。在這種情況下,您可以創建一個繼承自BusinessException
的新類,如下代碼塊所示
public class OrganizationNameAlreadyExistsException : BusinessException
{
public string Name { get; private set; }
public OrganizationNameAlreadyExistsException(string name) : base(EventHubErrorCodes.OrganizationNameAlreadyExists)
{
Name = name; WithData("Name", name);
}
}
在本例中,OrganizationNameAlreadyExistsException
是一個自定義業務異常類。它在構造函數中使用組織的名稱。拋出這個異常非常簡單:
throw new OrganizationNameAlreadyExistsException(name);
這種用法比使用自定義數據引發BusinessException
異常更簡單,因爲開發人員可能會忘記設置自定義數據。當您在多個位置拋出相同的異常時,它還可以減少代碼重複。
異常日誌記錄
如異常處理開頭所述,ABP會自動記錄所有異常:業務異常、授權和驗證異常以警告級別(Warning
級別),其他錯誤的警告級別默認是Error
級別。
我們可以實現IHasLogLevel
接口,爲異常類設置不同的日誌級別:
public class MyException : Exception, IHasLogLevel {
public LogLevel LogLevel { get; set; } = LogLevel.Warning;
//...
}
MyException
類實現了具有Warning
級別的IHasLogLevel
接口。如果拋出MyException
異常,ABP支持寫入警告日誌。
還可以爲異常寫入其他日誌,您可以實現IExceptionWithSelfLogging
接口來編寫其他日誌,如下所示:
public class MyException : Exception, IExceptionWithSelfLogging {
public void Log(ILogger logger) {
//...log additional info
}
}
HTTP狀態代碼
ABP盡最大努力爲已知的異常類型返回正確的HTTP狀態碼,如下所示:
401
(unauthorized-未經授權) :用戶尚未登錄, 對應AbpAuthorizationException
403
(forbidden-禁止) :用戶已登錄, 對應AbpAuthorizationException
400
(bad request-錯誤請求) 對應AbpValidationException
404
(not found-未找到) 對應EntityNotFoundException
403
(forbidden-禁止) 對應UserFriendlyException/BusinessException
501
(not implemented-未實現) 對應NotImplementedException
500
(internal server error-服務器內部錯誤) 對應其他異常
如果要爲異常返回自定義一個HTTP狀態碼,可以將錯誤代碼映射到HTTP狀態代碼,如以下配置所示:
services.Configure(options => {options.Map(EventHubErrorCodes.OrganizationNameAlreadyExists,HttpStatusCode.Conflict); });
建議在解決方案的Web或HTTP API層中進行配置。
總結
在本章中,我們探討了業務應用中實現的橫切關注點,包括授權,驗證和異常處理。下一章將介紹一些ABP的基本功能,如自動審計日誌和數據過濾。