[Abp vNext 源码分析] - 7. 权限与验证(干货)
一、简要说明
在上篇文章里面,我们在 ApplicationService
当中看到了权限检测代码,通过注入 IAuthorizationService
就可以实现权限检测。不过跳转到源码才发现,这个接口是 ASP.NET Core 原生提供的 “基于策略” 的权限验证接口,这就说明 ABP vNext 基于原生的授权验证框架进行了自定义扩展。
让我们来看一下 Volo.Abp.Ddd.Application 项目的依赖结构(权限相关)。
本篇文章下面的内容基本就会围绕上述框架模块展开,本篇文章通篇较长,因为还涉及到 .NET Core Identity 与 IdentityServer4 这两部分。关于这两部分的内容,我会在本篇文章大概讲述 ABP vNext 的实现,关于更加详细的内容,请查阅官方文档或其他博主的博客。
二、源码分析
ABP vNext 关于权限验证和权限定义的部分,都存放在 Volo.Abp.Authorization 和 Volo.Abp.Security 模块内部。源码分析我都比较喜欢倒推,即通过实际的使用场景,反向推导 基础实现,所以后面文章编写的顺序也将会以这种方式进行。
2.1 Security 基础组件库
这里我们先来到 Volo.Abp.Security,因为这个模块代码和类型都是最少的。这个项目都没有模块定义,说明里面的东西都是定义的一些基础组件。
2.1.1 Claims 与 Identity 的快捷访问
先从第一个扩展方法开始,这个扩展方法里面比较简单,它主要是提供对 ClaimsPrincipal
和 IIdentity
的快捷访问方法。比如我要从 ClaimsPrincipal
/ IIdentity
获取租户 Id、用户 Id 等。
public static class AbpClaimsIdentityExtensions
{
public static Guid? FindUserId([NotNull] this ClaimsPrincipal principal)
{
Check.NotNull(principal, nameof(principal));
// 根据 AbpClaimTypes.UserId 查找对应的值。
var userIdOrNull = principal.Claims?.FirstOrDefault(c => c.Type == AbpClaimTypes.UserId);
if (userIdOrNull == null || userIdOrNull.Value.IsNullOrWhiteSpace())
{
return null;
}
// 返回 Guid 对象。
return Guid.Parse(userIdOrNull.Value);
}
2.1.2 未授权异常的定义
这个异常我们在老版本 ABP 里面也见到过,它就是 AbpAuthorizationException
。只要有任何未授权的操作,都会导致该异常被抛出。后面我们在讲解 ASP.NET Core MVC 的时候就会知道,在默认的错误码处理中,针对于程序抛出的 AbpAuthorizationException
,都会视为 403 或者 401 错误。
public class DefaultHttpExceptionStatusCodeFinder : IHttpExceptionStatusCodeFinder, ITransientDependency
{
// ... 其他代码
public virtual HttpStatusCode GetStatusCode(HttpContext httpContext, Exception exception)
{
// ... 其他代码
// 根据 HTTP 协议对于状态码的定义,401 表示的是没有登录的用于尝试访问受保护的资源。而 403 则表示用户已经登录,但他没有目标资源的访问权限。
if (exception is AbpAuthorizationException)
{
return httpContext.User.Identity.IsAuthenticated
? HttpStatusCode.Forbidden
: HttpStatusCode.Unauthorized;
}
// ... 其他代码
}
// ... 其他代码
}
就 AbpAuthorizationException
异常来说,它本身并不复杂,只是一个简单的异常而已。只是因为它的特殊含义,在 ABP vNext 处理异常时都会进行特殊处理。
只是在这里我说明一下,ABP vNext 将它所有的异常都设置为可序列化的,这里的可序列化不仅仅是将 Serialzable
标签打在类上就行了。ABP vNext 还创建了基于 StreamingContext
的构造函数,方便我们后续对序列化操作进行定制化处理。
关于运行时序列化的相关文章,可以参考 《CLR Via C#》第 24 章,我也编写了相应的 读书笔记 。
2.1.3 当前用户与客户端
开发人员经常会在各种地方需要获取当前的用户信息,ABP vNext 将当前用户封装到 ICurrentUser
与其实现 CurrentUser
当中,使用时只需要注入 ICurrentUser
接口即可。
我们首先康康 ICurrentUser
接口的定义:
public interface ICurrentUser
{
bool IsAuthenticated { get; }
[CanBeNull]
Guid? Id { get; }
[CanBeNull]
string UserName { get; }
[CanBeNull]
string PhoneNumber { get; }
bool PhoneNumberVerified { get; }
[CanBeNull]
string Email { get; }
bool EmailVerified { get; }
Guid? TenantId { get; }
[NotNull]
string[] Roles { get; }
[CanBeNull]
Claim FindClaim(string claimType);
[NotNull]
Claim[] FindClaims(string claimType);
[NotNull]
Claim[] GetAllClaims();
bool IsInRole(string roleName);
}
那么这些值是从哪儿来的呢?从带有 Claim
返回值的方法来看,肯定就是从 HttpContext.User
或者 Thread.CurrentPrincipal
里面拿到的。
那么它的实现就非常简单了,只需要注入 ABP vNext 为我们提供的 ICurrentPrincipalAccessor
访问器,我们就能够拿到这个身份容器(ClaimsPrincipal
)。
public class CurrentUser : ICurrentUser, ITransientDependency
{
// ... 其他代码
public virtual string[] Roles => FindClaims(AbpClaimTypes.Role).Select(c => c.Value).ToArray();
private readonly ICurrentPrincipalAccessor _principalAccessor;
public CurrentUser(ICurrentPrincipalAccessor principalAccessor)
{
_principalAccessor = principalAccessor;
}
// ... 其他代码
public virtual Claim[] FindClaims(string claimType)
{
// 直接使用 LINQ 查询对应的 Type 就能拿到上述信息。
return _principalAccessor.Principal?.Claims.Where(c => c.Type == claimType).ToArray() ?? EmptyClaimsArray;
}
// ... 其他代码
}
至于 CurrentUserExtensions
扩展类,里面只是对 ClaimsPrincipal
的搜索方法进行了多种封装而已。
PS:
除了
ICurrentUser
与ICurrentClient
之外,在 ABP vNext 里面还有ICurrentTenant
来获取当前租户信息。通过这三个组件,取代了老 ABP 框架的IAbpSession
组件,三个组件都没有IAbpSession.Use()
扩展方法帮助我们临时更改当前用户/租户。
2.1.4 ClaimsPrincipal 访问器
关于 ClaimsPrincipal 的内容,可以参考杨总的 《ASP.NET Core 之 Identity 入门》 进行了解,大致来说就是存有 Claim
信息的聚合对象。
关于 ABP vNext 框架预定义的 Claim Type 都存放在 AbpClaimTypes
类型里面的,包括租户 Id、用户 Id 等数据,这些玩意儿最终会被放在 JWT(JSON Web Token) 里面去。
一般来说 ClaimsPrincipal
里面都是从 HttpContext.User
或者 Thread.CurrentPrincipal
得到的,ABP vNext 为我们抽象出了一个快速访问接口 ICurrentPrincipalAccessor
。开发人员注入之后,就可以获得当前用户的 ClaimsPrincipal
对象。
public interface ICurrentPrincipalAccessor
{
ClaimsPrincipal Principal { get; }
}
对于 Thread.CurrentPrincipal
的实现:
public class ThreadCurrentPrincipalAccessor : ICurrentPrincipalAccessor, ISingletonDependency
{
public virtual ClaimsPrincipal Principal => Thread.CurrentPrincipal as ClaimsPrincipal;
}
而针对于 Http 上下文的实现,则是放在 Volo.Abp.AspNetCore 模块里面的。
public class HttpContextCurrentPrincipalAccessor : ThreadCurrentPrincipalAccessor
{
// 如果没有获取到数据,则使用 Thread.CurrentPrincipal。
public override ClaimsPrincipal Principal => _httpContextAccessor.HttpContext?.User ?? base.Principal;
private readonly IHttpContextAccessor _httpContextAccessor;
public HttpContextCurrentPrincipalAccessor(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
}
扩展知识:两者的区别?
Thread.CurrentPrincipal
可以设置/获得当前线程的 ClaimsPrincipal
数据,而 HttpContext?.User
一般都是被 ASP.NET Core 中间件所填充的。
最新的 ASP.NET Core 开发建议是不要使用 Thread.CurrentPrincipal
和 ClaimsPrincipal.Current
(内部实现还是使用的前者)。这是因为 Thread.CurrentPrincipal
是一个静态成员...而这个静态成员在异步代码中会出现各种问题,例如有以下代码:
// Create a ClaimsPrincipal and set Thread.CurrentPrincipal
var identity = new ClaimsIdentity();
identity.AddClaim(new Claim(ClaimTypes.Name, "User1"));
Thread.CurrentPrincipal = new ClaimsPrincipal(identity);
// Check the current user
Console.WriteLine($"Current user: {Thread.CurrentPrincipal?.Identity.Name}");
// For the method to complete asynchronously
await Task.Yield();
// Check the current user after
Console.WriteLine($"Current user: {Thread.CurrentPrincipal?.Identity.Name}");
当 await
执行完成之后会产生线程切换,这个时候 Thread.CurrentPrincipal 的值就是 null 了,这就会产生不可预料的后果。
如果你还想了解更多信息,可以参考以下两篇博文:
- DAVID PINE - 《WHAT HAPPENED TO MY THREAD.CURRENTPRINCIPAL》
- SCOTT HANSELMAN - 《System.Threading.Thread.CurrentPrincipal vs. System.Web.HttpContext.Current.User or why FormsAuthentication can be subtle》
2.1.5 字符串加密工具
这一套东西就比较简单了,是 ABP vNext 为我们提供的一套开箱即用组件。开发人员可以使用 IStringEncryptionService
来加密/解密你的字符串,默认实现是基于 Rfc2898DeriveBytes
的。关于详细信息,你可以阅读具体的代码,这里不再赘述。
2.2 权限与校验
在 Volo.Abp.Authorization 模块里面就对权限进行了具体定义,并且基于 ASP.NET Core Authentication 进行无缝集成。如果读者对于 ASP.NET Core 认证和授权不太了解,可以去学习一下 雨夜朦胧 大神的《ASP.NET Core 认证于授权》系列文章,这里就不再赘述。
2.2.1 权限的注册
在 ABP vNext 框架里面,所有用户定义的权限都是通过继承 PermissionDefinitionProvider
,在其内部进行注册的。
public abstract class PermissionDefinitionProvider : IPermissionDefinitionProvider, ITransientDependency
{
public abstract void Define(IPermissionDefinitionContext context);
}
开发人员继承了这个 Provider 之后,在 Define()
方法里面就可以注册自己的权限了,这里我以 Blog 模块的简化 Provider 为例。
public class BloggingPermissionDefinitionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
var bloggingGroup = context.AddGroup(BloggingPermissions.GroupName, L("Permission:Blogging"));
// ... 其他代码。
var tags = bloggingGroup.AddPermission(BloggingPermissions.Tags.Default, L("Permission:Tags"));
tags.AddChild(BloggingPermissions.Tags.Update, L("Permission:Edit"));
tags.AddChild(BloggingPermissions.Tags.Delete, L("Permission:Delete"));
tags.AddChild(BloggingPermissions.Tags.Create, L("Permission:Create"));
var comments = bloggingGroup.AddPermission(BloggingPermissions.Comments.Default, L("Permission:Comments"));
comments.AddChild(BloggingPermissions.Comments.Update, L("Permission:Edit"));
comments.AddChild(BloggingPermissions.Comments.Delete, L("Permission:Delete"));
comments.AddChild(BloggingPermissions.Comments.Create, L("Permission:Create"));
}
// 使用本地化字符串进行文本显示。
private static LocalizableString L(string name)
{
return LocalizableString.Create<BloggingResource>(name);
}
}
从上面的代码就可以看出来,权限被 ABP vNext 分成了 权限组定义 和 权限定义,这两个东西我们后面进行重点讲述。那么这些 Provider 在什么时候被执行呢?找到权限模块的定义,可以看到如下代码:
[DependsOn(
typeof(AbpSecurityModule),
typeof(AbpLocalizationAbstractionsModule),
typeof(AbpMultiTenancyModule)
)]
public class AbpAuthorizationModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
// 在 AutoFac 进行组件注册的时候,根据组件的类型定义视情况绑定拦截器。
context.Services.OnRegistred(AuthorizationInterceptorRegistrar.RegisterIfNeeded);
// 在 AutoFac 进行组件注册的时候,根据组件的类型,判断是否是 Provider。
AutoAddDefinitionProviders(context.Services);
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
// 注册认证授权服务。
context.Services.AddAuthorization();
// 替换掉 ASP.NET Core 提供的权限处理器,转而使用 ABP vNext 提供的权限处理器。
context.Services.AddSingleton<IAuthorizationHandler, PermissionRequirementHandler>();
// 这一部分是添加内置的一些权限值检查,后面我们在将 PermissionChecker 的时候会提到。
Configure<PermissionOptions>(options =>
{
options.ValueProviders.Add<UserPermissionValueProvider>();
options.ValueProviders.Add<RolePermissionValueProvider>();
options.ValueProviders.Add<ClientPermissionValueProvider>();
});
}
private static void AutoAddDefinitionProviders(IServiceCollection services)
{
var definitionProviders = new List<Type>();
services.OnRegistred(context =>
{
if (typeof(IPermissionDefinitionProvider).IsAssignableFrom(context.ImplementationType))
{
definitionProviders.Add(context.ImplementationType);
}
});
// 将获取到的 Provider 传递给 PermissionOptions 。
services.Configure<PermissionOptions>(options =>
{
options.DefinitionProviders.AddIfNotContains(definitionProviders);
});
}
}
可以看到在注册组件的时候,ABP vNext 就会将这些 Provider 传递给 PermissionOptions
,我们根据 DefinitionProviders
字段找到有一个地方会使用到它,就是 PermissionDefinitionManager
类型的 CreatePermissionGroupDefinitions()
方法。
protected virtual Dictionary<string, PermissionGroupDefinition> CreatePermissionGroupDefinitions()
{
// 创建一个权限定义上下文。
var context = new PermissionDefinitionContext();
// 创建一个临时范围用于解析 Provider,Provider 解析完成之后即被释放。
using (var scope = _serviceProvider.CreateScope())
{
// 根据之前的类型,通过 IoC 进行解析出实例,指定各个 Provider 的 Define() 方法,会向权限上下文填充权限。
var providers = Options
.DefinitionProviders
.Select(p => scope.ServiceProvider.GetRequiredService(p) as IPermissionDefinitionProvider)
.ToList();
foreach (var provider in providers)
{
provider.Define(context);
}
}
// 返回权限组名称 - 权限组定义的字典。
return context.Groups;
}
你可能会奇怪,为什么返回的是一个权限组名字和定义的键值对,而不是返回的权限数据,我们之前添加的权限去哪儿了呢?
2.2.2 权限和权限组的定义
要搞清楚这个问题,我们首先要知道权限与权限组之间的关系是怎样的。回想我们之前在 Provider 里面添加权限的代码,首先我们是构建了一个权限组,然后往权限组里面添加的权限。权限组的作用就是将权限按照组的形式进行划分,方便代码进行访问于管理。
public class PermissionGroupDefinition
{
/// <summary>
/// 唯一的权限组标识名称。
/// </summary>
public string Name { get; }
// 开发人员针对权限组的一些自定义属性。
public Dictionary<string, object> Properties { get; }
// 权限所对应的本地化名称。
public ILocalizableString DisplayName
{
get => _displayName;
set => _displayName = Check.NotNull(value, nameof(value));
}
private ILocalizableString _displayName;
/// <summary>
/// 权限的适用范围,默认是租户/租主都适用。
/// 默认值: <see cref="MultiTenancySides.Both"/>
/// </summary>
public MultiTenancySides MultiTenancySide { get; set; }
// 权限组下面的所属权限。
public IReadOnlyList<PermissionDefinition> Permissions => _permissions.ToImmutableList();
private readonly List<PermissionDefinition> _permissions;
// 针对于自定义属性的快捷索引器。
public object this[string name]
{
get => Properties.GetOrDefault(name);
set => Properties[name] = value;
}
protected internal PermissionGroupDefinition(
string name,
ILocalizableString displayName = null,
MultiTenancySides multiTenancySide = MultiTenancySides.Both)
{
Name = name;
// 没有传递多语言串,则使用权限组的唯一标识作为显示内容。
DisplayName = displayName ?? new FixedLocalizableString(Name);
MultiTenancySide = multiTenancySide;
Properties = new Dictionary<string, object>();
_permissions = new List<PermissionDefinition>();
}
// 像权限组添加属于它的权限。
public virtual PermissionDefinition AddPermission(
string name,
ILocalizableString displayName = null,
MultiTenancySides multiTenancySide = MultiTenancySides.Both)
{
var permission = new PermissionDefinition(name, displayName, multiTenancySide);
_permissions.Add(permission);
return permission;
}
// 递归构建权限集合,因为定义的某个权限内部还拥有子权限。
public virtual List<PermissionDefinition> GetPermissionsWithChildren()
{
var permissions = new List<PermissionDefinition>();
foreach (var permission in _permissions)
{
AddPermissionToListRecursively(permissions, permission);
}
return permissions;
}
// 递归构建方法。
private void AddPermissionToListRecursively(List<PermissionDefinition> permissions, PermissionDefinition permission)
{
permissions.Add(permission);
foreach (var child in permission.Children)
{
AddPermissionToListRecursively(permissions, child);
}
}
public override string ToString()
{
return $"[{nameof(PermissionGroupDefinition)} {Name}]";
}
}
通过权限组的定义代码你就会知道,现在我们的所有权限都会归属于某个权限组,这一点从之前 Provider 的 IPermissionDefinitionContext
就可以看出来。在权限上下文内部只允许我们通过 AddGroup()
来添加一个权限组,之后再通过权限组的 AddPermission()
方法添加它里面的权限。
权限的定义类叫做 PermissionDefinition
,这个类型的构造与权限组定义类似,没有什么好说的。
public class PermissionDefinition
{
/// <summary>
/// 唯一的权限标识名称。
/// </summary>
public string Name { get; }
/// <summary>
/// 当前权限的父级权限,这个属性的值只可以通过 AddChild() 方法进行设置。
/// </summary>
public PermissionDefinition Parent { get; private set; }
/// <summary>
/// 权限的适用范围,默认是租户/租主都适用。
/// 默认值: <see cref="MultiTenancySides.Both"/>
/// </summary>
public MultiTenancySides MultiTenancySide { get; set; }
/// <summary>
/// 适用的权限值提供者,这块我们会在后面进行讲解,为空的时候则使用所有的提供者进行校验。
/// </summary>
public List<string> Providers { get; } //TODO: Rename to AllowedProviders?
// 权限的多语言名称。
public ILocalizableString DisplayName
{
get => _displayName;
set => _displayName = Check.NotNull(value, nameof(value));
}
private ILocalizableString _displayName;
// 获取权限的子级权限。
public IReadOnlyList<PermissionDefinition> Children => _children.ToImmutableList();
private readonly List<PermissionDefinition> _children;
/// <summary>
/// 开发人员针对权限的一些自定义属性。
/// </summary>
public Dictionary<string, object> Properties { get; }
// 针对于自定义属性的快捷索引器。
public object this[string name]
{
get => Properties.GetOrDefault(name);
set => Properties[name] = value;
}
protected internal PermissionDefinition(
[NotNull] string name,
ILocalizableString displayName = null,
MultiTenancySides multiTenancySide = MultiTenancySides.Both)
{
Name = Check.NotNull(name, nameof(name));
DisplayName = displayName ?? new FixedLocalizableString(name);
MultiTenancySide = multiTenancySide;
Properties = new Dictionary<string, object>();
Providers = new List<string>();
_children = new List<PermissionDefinition>();
}
public virtual PermissionDefinition AddChild(
[NotNull] string name,
ILocalizableString displayName = null,
MultiTenancySides multiTenancySide = MultiTenancySides.Both)
{
var child = new PermissionDefinition(
name,
displayName,
multiTenancySide)
{
Parent = this
};
_children.Add(child);
return child;
}
/// <summary>
/// 设置指定的自定义属性。
/// </summary>
public virtual PermissionDefinition WithProperty(string key, object value)
{
Properties[key] = value;
return this;
}
/// <summary>
/// 添加一组权限值提供者集合。
/// </summary>
public virtual PermissionDefinition WithProviders(params string[] providers)
{
if (!providers.IsNullOrEmpty())
{
Providers.AddRange(providers);