大家好,我是張飛洪,感謝您的閱讀,我會不定期和你分享學習心得,希望我的文章能成為你成長路上的墊腳石,讓我們一起精進。
授權、驗證、異常處理和日誌記錄等橫切關注點是每個系統的基本組成部分,它們對於確保系統的安全和良好執行至關重要。
實現橫切關注點會導致應用中的很多地方出現重複程式碼。此外,一次授權或驗證檢查缺失可能會導致整個系統崩潰。
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的基本功能,如自動審計日誌和資料過濾。