探索ABP基礎架構的橫切關注點

張飛洪[廈門]發表於2023-02-10

大家好,我是張飛洪,感謝您的閱讀,我會不定期和你分享學習心得,希望我的文章能成為你成長路上的墊腳石,讓我們一起精進。

授權、驗證、異常處理和日誌記錄等橫切關注點是每個系統的基本組成部分,它們對於確保系統的安全和良好執行至關重要。

實現橫切關注點會導致應用中的很多地方出現重複程式碼。此外,一次授權或驗證檢查缺失可能會導致整個系統崩潰。

ABP框架的主要目標之一是使你的應用“不要重複自己”(DRY),ASP.NET Core已經為一些跨領域的問題提供了一個良好的基礎設施,但ABP進一步實現了自動化,讓使用更加容易。

本章探討了ABP的基礎設施:

  • 認證授權
  • 使用者驗證
  • 異常處理

認證和授權是安全中的兩個主要概念。身份驗證是識別當前使用者的過程,授權用於允許或禁止使用者執行應用的特定操作。

ASP.NET Core系統本身提供了一種高階而靈活的認證和授權,ABP框架的認證授權與ASP.NET Core100%相容,並進行了一定的擴充套件,它允許將許可權授予角色和使用者,它還允許在客戶端進行許可權檢查。

簡單授權檢查

最簡單的場景,只允許登入的使用者執行特定操作。
[Authorize]屬性不帶任何引數,只檢查當前使用者是否已透過身份驗證(登入)。

請參見以下控制器(MVC):

public class ProductController : Controller {     
    public async Task GetListAsync(){}     
    [Authorize]         
    public async Task CreateAsync(ProductCreationDto input){}             
    [Authorize]     
    public async Task DeleteAsync(Guid id){} 
}

在本例中,CreateAsyncDeleteAsync操作僅允許透過身份驗證的使用者使用,假設匿名使用者(尚未登入的使用者)嘗試執行這些操作,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響應。IsGrantedAsyncCheckAsync方法是ABP框架定義的有用的擴充套件方法。

[warning] 提示:從AbpController繼承

建議從AbpController類而不是標準Controller類派生。因為它內部做了擴充套件,定義了一些有用的屬性。比如,它有AuthorizationService屬性(屬於IAuthorizationService型別),您可以直接使用它,無需手動注入IAuthorizationService介面。

伺服器上的許可權檢查是一種常見的方法。但是,您可能還需要檢查客戶端的許可權。

4 客戶端許可權

ABP公開了一個標準HTTP API,其URL為/api/abp/application-configuration,返回包含本地化文字、設定、許可權等的JSON資料。客戶端可以使用該API來檢查許可權或在客戶端執行本地化。

不同的客戶端型別可能會提供不同的服務來檢查許可權。例如,在MVC/Razor Pages中,可以使用abp.authJavaScript 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>,其中TProductCreationRequirement型別。在本例中,我只是檢查了當前使用者是否擁有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註冊為單例服務。

現在,假設我對ControllerAction使用[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有一個自定義規則:如果IsDraftfalse,並且圖片路徑為控,則提示需要上傳圖片。
如果需要從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。我們透過指定驗證錯誤來丟擲AbpValidationExceptionValidationResult表示驗證錯誤;它的第一個建構函式引數是驗證錯誤訊息,第二個引數(可選)是DTO屬性的名稱。

一旦您或ABP驗證系統丟擲AbpValidationException異常,ABP異常處理系統將捕獲並處理它。

禁用驗證

可以使用[DisableValidation]在方法或類級別繞過ABP驗證系統,如下例所示:

[DisableValidation] 
public async Task CreateAsync(ProductCreationDto input) { }

在本例中,CreateAsync方法用[DisableValidation]修飾,因此ABP不會對輸入物件執行任何自動驗證。
如果對類使用[DisableValidation],則該類的所有方法的驗證都將被禁用。在這種情況下,可以對某個方法使用[EnableValidation],以便僅對該特定方法啟用驗證。

當禁用方法的自動驗證時,仍然可以執行自定義驗證邏輯並丟擲AbpValidationException,如前一節所述。

其他型別的驗證

除了對Controller ActionsRazor 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的基本功能,如自動審計日誌和資料過濾。

相關文章