大家好,我是张飞洪,感谢您的阅读,我会不定期和你分享学习心得,希望我的文章能成为你成长路上的垫脚石,让我们一起精进。
授权、验证、异常处理和日志记录等横切关注点是每个系统的基本组成部分,它们对于确保系统的安全和良好运行至关重要。
实现横切关注点会导致应用中的很多地方出现重复代码。此外,一次授权或验证检查缺失可能会导致整个系统崩溃。
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的基本功能,如自动审计日志和数据过滤。