[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);
}
return this;
}
public override string ToString()
{
return $"[{nameof(PermissionDefinition)} {Name}]";
}
}
2.2.3 权限管理器
继续回到权限管理器,权限管理器的接口定义是 IPermissionDefinitionManager
,从接口的方法定义来看,都是获取权限的方法,说明权限管理器主要提供给其他组件进行权限校验操作。
public interface IPermissionDefinitionManager
{
// 根据权限定义的唯一标识获取权限,一旦不存在就会抛出 AbpException 异常。
[NotNull]
PermissionDefinition Get([NotNull] string name);
// 根据权限定义的唯一标识获取权限,如果权限不存在,则返回 null。
[CanBeNull]
PermissionDefinition GetOrNull([NotNull] string name);
// 获取所有的权限。
IReadOnlyList<PermissionDefinition> GetPermissions();
// 获取所有的权限组。
IReadOnlyList<PermissionGroupDefinition> GetGroups();
}
接着我们来回答 2.2.1 末尾提出的问题,权限组是根据 Provider 自动创建了,那么权限呢?其实我们在权限管理器里面拿到了权限组,权限定义就很好构建了,直接遍历所有权限组拿它们的 Permissions
属性构建即可。
protected virtual Dictionary<string, PermissionDefinition> CreatePermissionDefinitions()
{
var permissions = new Dictionary<string, PermissionDefinition>();
// 遍历权限定义组,这个东西在之前就已经构建好了。
foreach (var groupDefinition in PermissionGroupDefinitions.Values)
{
// 递归子级权限。
foreach (var permission in groupDefinition.Permissions)
{
AddPermissionToDictionaryRecursively(permissions, permission);
}
}
// 返回权限唯一标识 - 权限定义 的字典。
return permissions;
}
protected virtual void AddPermissionToDictionaryRecursively(
Dictionary<string, PermissionDefinition> permissions,
PermissionDefinition permission)
{
if (permissions.ContainsKey(permission.Name))
{
throw new AbpException("Duplicate permission name: " + permission.Name);
}
permissions[permission.Name] = permission;
foreach (var child in permission.Children)
{
AddPermissionToDictionaryRecursively(permissions, child);
}
}
2.2.4 授权策略提供者的实现
我们发现 ABP vNext 自己实现了 IAbpAuthorizationPolicyProvider
接口,实现的类型就是 AbpAuthorizationPolicyProvider
。
这个类型它是继承的 DefaultAuthorizationPolicyProvider
,重写了 GetPolicyAsync()
方法,目的就是将 PermissionDefinition
转换为 AuthorizationPolicy
。
如果去看了 雨夜朦胧 大神的博客,就知道我们一个授权策略可以由多个条件构成。也就是说某一个 AuthorizationPolicy
可以拥有多个限定条件,当所有限定条件被满足之后,才能算是通过权限验证,例如以下代码。
public void ConfigureService(IServiceCollection services)
{
services.AddAuthorization(options =>
{
options.AddPolicy("User", policy => policy
.RequireAssertion(context => context.User.HasClaim(c => (c.Type == "EmployeeNumber" || c.Type == "Role")))
);
// 这里的意思是,用户角色必须是 Admin,并且他的用户名是 Alice,并且必须要有类型为 EmployeeNumber 的 Claim。
options.AddPolicy("Employee", policy => policy
.RequireRole("Admin")
.RequireUserName("Alice")
.RequireClaim("EmployeeNumber")
.Combine(commonPolicy));
});
}
这里的 RequireRole()
、RequireUserName()
、RequireClaim()
都会生成一个 IAuthorizationRequirement
对象,它们在内部有不同的实现规则。
public AuthorizationPolicyBuilder RequireClaim(string claimType)
{
if (claimType == null)
{
throw new ArgumentNullException(nameof(claimType));
}
// 构建了一个 ClaimsAuthorizationRequirement 对象,并添加到策略的 Requirements 组。
Requirements.Add(new ClaimsAuthorizationRequirement(claimType, allowedValues: null));
return this;
}
这里我们 ABP vNext 则是使用的 PermissionRequirement
作为一个限定条件。
public override async Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
var policy = await base.GetPolicyAsync(policyName);
if (policy != null)
{
return policy;
}
var permission = _permissionDefinitionManager.GetOrNull(policyName);
if (permission != null)
{
// TODO: 可以使用缓存进行优化。
// 通过 Builder 构建一个策略。
var policyBuilder = new AuthorizationPolicyBuilder(Array.Empty<string>());
// 创建一个 PermissionRequirement 对象添加到限定条件组中。
policyBuilder.Requirements.Add(new PermissionRequirement(policyName));
return policyBuilder.Build();
}
return null;
}
与 ClaimsAuthorizationRequirement
不同的是,ABP vNext 并没有将限定条件处理器和限定条件定义放在一起实现,而是分开的,分别构成了 PermissionRequirement
和 PermissionRequirementHandler
,后者在模块配置的时候被注入到 IoC 里面。
PS:
对于 Handler 来说,我们可以编写多个 Handler 注入到 IoC 容器内部,如下代码:
services.AddSingleton<IAuthorizationHandler, BadgeEntryHandler>();
services.AddSingleton<IAuthorizationHandler, HasTemporaryStickerHandler>();
首先看限定条件 PermissionRequirement
的定义,非常简单。
public class PermissionRequirement : IAuthorizationRequirement
{
public string PermissionName { get; }
public PermissionRequirement([NotNull]string permissionName)
{
Check.NotNull(permissionName, nameof(permissionName));
PermissionName = permissionName;
}
}
在限定条件内部,我们只用了权限的唯一标识来进行处理,接下来看一下权限处理器。
public class PermissionRequirementHandler : AuthorizationHandler<PermissionRequirement>
{
// 这里通过权限检查器来确定当前用户是否拥有某个权限。
private readonly IPermissionChecker _permissionChecker;
public PermissionRequirementHandler(IPermissionChecker permissionChecker)
{
_permissionChecker = permissionChecker;
}
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionRequirement requirement)
{
// 如果当前用户拥有某个权限,则通过 Contxt.Succeed() 通过授权验证。
if (await _permissionChecker.IsGrantedAsync(context.User, requirement.PermissionName))
{
context.Succeed(requirement);
}
}
}
2.2.5 权限检查器
在上面的处理器我们看到了,ABP vNext 是通过权限检查器来校验某个用户是否满足某个授权策略,先看一下 IPermissionChecker
接口的定义,基本都是传入身份证(ClaimsPrincipal
)和需要校验的权限进行处理。
public interface IPermissionChecker
{
Task<bool> IsGrantedAsync([NotNull]string name);
Task<bool> IsGrantedAsync([CanBeNull] ClaimsPrincipal claimsPrincipal, [NotNull]string name);
}
第一个方法内部就是调用的第二个方法,只不过传递的身份证是通过 ICurrentPrincipalAccessor
拿到的,所以我们的核心还是看第二个方法的实现。
public virtual async Task<bool> IsGrantedAsync(ClaimsPrincipal claimsPrincipal, string name)
{
Check.NotNull(name, nameof(name));
var permission = PermissionDefinitionManager.Get(name);
var multiTenancySide = claimsPrincipal?.GetMultiTenancySide()
?? CurrentTenant.GetMultiTenancySide();
// 检查传入的权限是否允许当前的用户模式(租户/租主)进行访问。
if (!permission.MultiTenancySide.HasFlag(multiTenancySide))
{
return false;
}
var isGranted = false;
// 这里是重点哦,这个权限值检测上下文是之前没有说过的东西,说白了就是针对不同维度的权限检测。
// 之前这部分东西是通过权限策略下面的 Requirement 提供的,这里 ABP vNext 将其抽象为 PermissionValueProvider。
var context = new PermissionValueCheckContext(permission, claimsPrincipal);
foreach (var provider in PermissionValueProviderManager.ValueProviders)
{
// 如果指定的权限允许的权限值提供者集合不包含当前的 Provider,则跳过处理。
if (context.Permission.Providers.Any() &&
!context.Permission.Providers.Contains(provider.Name))
{
continue;
}
// 调用 Provider 的检测方法,传入身份证明和权限定义进行具体校验。
var result = await provider.CheckAsync(context);
// 根据返回的结果,判断是否通过了权限校验。
if (result == PermissionGrantResult.Granted)
{
isGranted = true;
}
else if (result == PermissionGrantResult.Prohibited)
{
return false;
}
}
// 返回 true 说明已经授权,返回 false 说明是没有授权的。
return isGranted;
}
2.2.6 PermissionValueProvider
在模块配置方法内部,可以看到通过 Configure<PermissionOptions>()
方法添加了三个 PermissionValueProvider
,即 UserPermissionValueProvider
、RolePermissionValueProvider
、ClientPermissionValueProvider
。在它们的内部实现,都是通过 IPermissionStore
从持久化存储 检查传入的用户是否拥有某个权限。
这里我们以 UserPermissionValueProvider
为例,来看看它的实现方法。
public class UserPermissionValueProvider : PermissionValueProvider
{
// 提供者的名称。
public const string ProviderName = "User";
public override string Name => ProviderName;
public UserPermissionValueProvider(IPermissionStore permissionStore)
: base(permissionStore)
{
}
public override async Task<PermissionGrantResult> CheckAsync(PermissionValueCheckContext context)
{
// 从传入的 Principal 中查找 UserId,不存在则说明没有定义,视为未授权。
var userId = context.Principal?.FindFirst(AbpClaimTypes.UserId)?.Value;
if (userId == null)
{
return PermissionGrantResult.Undefined;
}
// 调用 IPermissionStore 从持久化存储中,检测指定权限在某个提供者下面是否已经被授予了权限。
// 如果被授予了权限, 则返回 true,没有则返回 false。
return await PermissionStore.IsGrantedAsync(context.Permission.Name, Name, userId)
? PermissionGrantResult.Granted
: PermissionGrantResult.Undefined;
}
}
这里我们先不讲 IPermissionStore
的具体实现,就上述代码来看,ABP vNext 是将权限定义放在了一个管理容器(IPermissionDeftiionManager
)。然后又实现了自定义的策略处理器和策略,在处理器的内部又通过 IPermissionChecker
根据不同的 PermissionValueProvider
结合 IPermissionStore
实现了指定用户标识到权限的检测功能。
2.2.7 权限验证拦截器
权限验证拦截器的注册都是在 AuthorizationInterceptorRegistrar
的 RegisterIfNeeded()
方法内实现的,只要类型的任何一个方法标注了 AuthorizeAttribute
特性,就会被关联拦截器。
private static bool AnyMethodHasAuthorizeAttribute(Type implementationType)
{
return implementationType
.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Any(HasAuthorizeAttribute);
}
private static bool HasAuthorizeAttribute(MemberInfo methodInfo)
{
return methodInfo.IsDefined(typeof(AuthorizeAttribute), true);
}
拦截器和类型关联之后,会通过 IMethodInvocationAuthorizationService
的 CheckAsync()
方法校验调用者是否拥有指定权限。
public override async Task InterceptAsync(IAbpMethodInvocation invocation)
{
// 防止重复检测。
if (AbpCrossCuttingConcerns.IsApplied(invocation.TargetObject, AbpCrossCuttingConcerns.Authorization))
{
await invocation.ProceedAsync();
return;
}
// 将被调用的方法传入,验证是否允许访问。
await AuthorizeAsync(invocation);
await invocation.ProceedAsync();
}
protected virtual async Task AuthorizeAsync(IAbpMethodInvocation invocation)
{
await _methodInvocationAuthorizationService.CheckAsync(
new MethodInvocationAuthorizationContext(
invocation.Method
)
);
}
在具体的实现当中,首先检测方法是否标注了 IAllowAnonymous
特性,标注了则说明允许匿名访问,直接返回不做任何处理。否则就会从方法获取实现了 IAuthorizeData
接口的特性,从里面拿到 Policy
值,并通过 IAuthorizationService
进行验证。
protected async Task CheckAsync(IAuthorizeData authorizationAttribute)
{
if (authorizationAttribute.Policy == null)
{
// 如果当前调用者没有进行认证,则抛出未登录的异常。
if (!_currentUser.IsAuthenticated && !_currentClient.IsAuthenticated)
{
throw new AbpAuthorizationException("Authorization failed! User has not logged in.");
}
}
else
{
// 通过 IAuthorizationService 校验当前用户是否拥有 authorizationAttribute.Policy 权限。
await _authorizationService.CheckAsync(authorizationAttribute.Policy);
}
}
针对于 IAuthorizationService
,ABP vNext 还是提供了自己的实现 AbpAuthorizationService
,里面没有重写什么方法,而是提供了两个新的属性,这两个属性是为了方便实现 AbpAuthorizationServiceExtensions
提供的扩展方法,这里不再赘述。
三、总结
关于权限与验证部分我就先讲到这儿,后续文章我会更加详细地为大家分析 ABP vNext 是如何进行权限管理,又是如何将 ABP vNext 和 ASP.NET Identity 、IdentityServer4 进行集成的。
[Abp vNext 源码分析] - 7. 权限与验证的更多相关文章
- [Abp vNext 源码分析] - 文章目录
一.简要介绍 ABP vNext 是 ABP 框架作者所发起的新项目,截止目前 (2019 年 2 月 18 日) 已经拥有 1400 多个 Star,最新版本号为 v 0.16.0 ,但还属于预览版 ...
- [Abp vNext 源码分析] - 5. DDD 的领域层支持(仓储、实体、值对象)
一.简要介绍 ABP vNext 框架本身就是围绕着 DDD 理念进行设计的,所以在 DDD 里面我们能够见到的实体.仓储.值对象.领域服务,ABP vNext 框架都为我们进行了实现,这些基础设施都 ...
- [Abp vNext 源码分析] - 6. DDD 的应用层支持 (应用服务)
一.简要介绍 ABP vNext 针对于应用服务层,为我们单独设计了一个模块进行实现,即 Volo.Abp.Ddd.Application 模块. PS:最近博主也是在恶补 DDD 相关的知识,这里推 ...
- [Abp vNext 源码分析] - 11. 用户的自定义参数与配置
一.简要说明 文章信息: 基于的 ABP vNext 版本:1.0.0 创作日期:2019 年 10 月 23 日晚 更新日期:暂无 ABP vNext 针对用户可编辑的配置,提供了单独的 Volo. ...
- [Abp vNext 源码分析] - 19. 多租户
一.简介 ABP vNext 原生支持多租户体系,可以让开发人员快速地基于框架开发 SaaS 系统.ABP vNext 实现多租户的思路也非常简单,通过一个 TenantId 来分割各个租户的数据,并 ...
- [Abp vNext 源码分析] - 21. 界面与文字的本地化
一.简介 ABP vNext 提供了全套的本地化字符串支持,具体用法可以参考官方使用文档.vNext 本身是对 Microsoft 提供的本地化组件进行了实现,通过 JSON 文件提供本地化源,这一点 ...
- [Abp vNext 源码分析] - 20. 电子邮件与短信支持
一.简介 ABP vNext 使用 Volo.Abp.Sms 包和 Volo.Abp.Emailing 包将短信和电子邮件作为基础设施进行了抽象,开发人员仅需要在使用的时候注入 ISmsSender ...
- [Abp vNext 源码分析] - 3. 依赖注入与拦截器
一.简要说明 ABP vNext 框架在使用依赖注入服务的时候,是直接使用的微软提供的 Microsoft.Extensions.DependencyInjection 包.这里与原来的 ABP 框架 ...
- [Abp vNext 源码分析] - 2. 模块系统的变化
一.简要说明 本篇文章主要分析 Abp vNext 当中的模块系统,从类型构造层面上来看,Abp vNext 当中不再只是单纯的通过 AbpModuleManager 来管理其他的模块,它现在则是 I ...
随机推荐
- Google 的 Java 编码规范,参考学习!
这份文档是 Google Java 编程风格规范的完整定义.当且仅当一个 Java 源文件符合此文档中的规则, 我们才认为它符合 Google 的 Java 编程风格. 与其它的编程风格指南一样,这里 ...
- 阿里云域名的ssl证书申请与腾讯服务器域名的证书安装
阿里云域名中的SSL证书申请,腾讯云服务器中的证书安装: 因为公司的与域名都在阿里云上面,服务器却用的是腾讯云的.记得前2年用阿里云管理平台的时候,域名的SSL证书都很好找,也许是长时间不用,找SSL ...
- C++学习书籍推荐《C++编程思想第二版第一卷》下载
百度云及其他网盘下载地址:点我 编辑推荐 “经典原版书库”是响应教育部提出的使用原版国外教材的号召,为国内高校的计算机教学度身订造的.<C++编程思想>(英文版第2版)是书库中的一本,在广 ...
- Java多线程(六):wait(),notify()和notifyAll()
wait(),notify()和notifyAll()介绍 1.wait() 使当前线程等待,直到另一个线程调用notify(),notifyAll()或者中断,当前线程调用wait()之前必须持有锁 ...
- vue-cli · Failed to download repo vuejs-templates/webpack: self signed certificate in certificate chain
vue init webpack <Project name> 报错: vue-cli · Failed to download repo vuejs-templates/webpack: ...
- idea中的beautiful插件-自动生成对象set方法
1. 描述 从前端获取VO对象后,好多时候又要生成数据库对象,需要进行赋值,一个个写很浪费时间,介绍一款idea中的beautiful插件,代码开发过程中自动生成对象的set方法,很好用. 2 .插件 ...
- 第二章 Session会话管理
采用网址重写的缺点: 在有些Web浏览器中,URL限制为2000个字符. 仅当有链接要插入值时,值才能转换成后面的资源.此外,要把值添加到静态页面的链接中,可不是一件容易的事情. 网址重写必须在服务器 ...
- I/O:FileLock
FileLock: /* 文件锁定要么是独占的,要么是共享的.共享锁定可阻止其他并发运行的程序获取重叠的 独占锁定,但是允许该程序获取重叠的共享锁定.独占锁定则阻止其他程序获取任一类型的重叠锁 定.一 ...
- Mllib数据类型(密集向量和稀疏向量)
1.局部向量 Mllib支持2种局部向量类型:密集向量(dense)和稀疏向量(sparse). 密集向量由double类型的数组支持,而稀疏向量则由两个平行数组支持. example: 向量(5.2 ...
- BI之路学习笔记2--SSIS/ETL设计练习三:《DB->定期生成excel表》
上次笔记记到,用sql任务给参数赋值,映射到变量,然后把数据流任务放到序列容器中进行执行,可以定期生成excel, 现在的问题是: 在EXCEL目标编辑过程中,必须选定某一个特定的excel目标,这样 ...