[Abp vNext 源码分析] - 7. 权限与验证

1、简要说明

在上篇文章里面,咱们在 ApplicationService 当中看到了权限检测代码,经过注入 IAuthorizationService 就能够实现权限检测。不过跳转到源码才发现,这个接口是 ASP.NET Core 原生提供的 “基于策略” 的权限验证接口,这就说明 ABP vNext 基于原生的受权验证框架进行了自定义扩展。html

让咱们来看一下 Volo.Abp.Ddd.Application 项目的依赖结构(权限相关)。缓存

本篇文章下面的内容基本就会围绕上述框架模块展开,本篇文章通篇较长,由于还涉及到 .NET Core IdentityIdentityServer4 这两部分。关于这两部分的内容,我会在本篇文章大概讲述 ABP vNext 的实现,关于更加详细的内容,请查阅官方文档或其余博主的博客。框架

2、源码分析

ABP vNext 关于权限验证和权限定义的部分,都存放在 Volo.Abp.AuthorizationVolo.Abp.Security 模块内部。源码分析我都比较喜欢倒推,即经过实际的使用场景,反向推导 基础实现,因此后面文章编写的顺序也将会以这种方式进行。异步

2.1 Security 基础组件库

这里咱们先来到 Volo.Abp.Security,由于这个模块代码和类型都是最少的。这个项目都没有模块定义,说明里面的东西都是定义的一些基础组件。async

2.1.1 Claims 与 Identity 的快捷访问

先从第一个扩展方法开始,这个扩展方法里面比较简单,它主要是提供对 ClaimsPrincipalIIdentity 的快捷访问方法。好比我要从 ClaimsPrincipal / IIdentity 获取租户 Id、用户 Id 等。ide

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:

除了 ICurrentUserICurrentClient 以外,在 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.CurrentPrincipalClaimsPrincipal.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 了,这就会产生不可预料的后果。

若是你还想了解更多信息,能够参考如下两篇博文:

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 并无将限定条件处理器和限定条件定义放在一块儿实现,而是分开的,分别构成了 PermissionRequirementPermissionRequirementHandler ,后者在模块配置的时候被注入到 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 ,即 UserPermissionValueProviderRolePermissionValueProviderClientPermissionValueProvider 。在它们的内部实现,都是经过 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 权限验证拦截器

权限验证拦截器的注册都是在 AuthorizationInterceptorRegistrarRegisterIfNeeded() 方法内实现的,只要类型的任何一个方法标注了 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);
}

拦截器和类型关联以后,会经过 IMethodInvocationAuthorizationServiceCheckAsync() 方法校验调用者是否拥有指定权限。

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 提供的扩展方法,这里再也不赘述。

3、总结

关于权限与验证部分我就先讲到这儿,后续文章我会更加详细地为你们分析 ABP vNext 是如何进行权限管理,又是如何将 ABP vNext 和 ASP.NET Identity 、IdentityServer4 进行集成的。

相关文章
相关标签/搜索