Abp 源码分析:十二、多租户体系与权限验证

0.简介

承接上篇文章我们会在这篇文章详细解说一下 Abp 是如何结合 IPermissionCheckerIFeatureChecker 来实现一个完整的多租户系统的权限校验的。

1.多租户的概念

多租户系统又被称之为 Saas ,比如阿里云就是一个典型的多租户系统,用户本身就是一个租户,可以在上面购买自己的 ECS 实例,并且自己的数据与其他使用者(租户)所隔绝,两者的数据都是不可见的。

那么 Abp 是如何实现数据隔离的呢?

1.1 单部署-单数据库

如果你的软件系统仅部署一个实例,并且所有租户的数据都是存放在一个数据库里面的,那么可以通过一个 TenantId (租户 Id) 来进行数据隔离。那么当我们执行 SELECT 操作的时候就会附加上当前登录用户租户 Id 作为过滤条件,那么查出来的数据也仅仅是当前租户的数据,而不会查询到其他租户的数据。

TIM–20180814084606

1.2 单部署-多数据库

Abp 还提供了另外一种方式,即为每一个租户提供一个单独的数据库,在用户登录的时候根据用户对应的租户 ID,从一个数据库连接映射表获取到当前租户对应的数据库连接字符串,并且在查询数据与写入数据的时候,不同租户操作的数据库是不一样的。

TIM–20180814084620

2.多租户系统的权限验证

从上一篇文章我们知道了在权限过滤器与权限拦截器当中,最终会使用 IFeatureCheckerIPermissionChecker 来进行权限校验,并且它还持久一个用户会话状态 IAbpSession 用于存储识别当前访问网站的用户是谁。

2.1 用户会话状态

基本做过网站程序开发的同学都知道用于区分每一个用户,我们需要通过 Session 来保存当前用户的状态,以便进行权限验证或者其他操作。而 Abp 框架则为我们定义了一个统一的会话状态接口 IAbpSession ,用于标识当前用户的状态。在其接口当中主要定义了三个重要的属性,第一个 UserId (用户 Id),第二个就是 TenantId (租户 Id),以及用于确定当前用户是租户还是租主的 MultiTenancySides 属性。

除此之外,还拥有一个 Use() 方法,用户在某些时候临时替换掉当前用户的 UserIdTenantId 的值,这个方法在我的《Abp + Grpc 如何实现用户会话状态传递》文章当中有讲到过。

而针对这个方法的实现又可以扯出一大堆知识,这块我们放在后面再进行精讲,这里我们还是主要通篇讲解一下多租户体系下的数据过滤与权限验证。

1534173881737

2.1.1 默认会话状态的实现

IAbpSession 当中的值默认是从 JWT 当中取得的,这取决于它的默认实现 ClaimsAbpSession,它还继承了一个抽象父类 AbpSessionBase ,这个父类主要是实现了 Use() 方法,这里略过。

在其默认实现里面,重载了 UserIdTenantId 的获取方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public override long? UserId
{
    get
    {
    	// ... 其他代码
        var userIdClaim = PrincipalAccessor.Principal?.Claims.FirstOrDefault(c => c.Type == AbpClaimTypes.UserId);
        // ... 其他代码
        
        long userId;
        if (!long.TryParse(userIdClaim.Value, out userId)) return null;

        return userId;
    }
}

可以看到这里是通过 PrincipalAccessor 从当前请求的请求头中获取 Token ,并从 Claims 里面获取 Type 值为 AbpClaimTypes.UserId 的对象,将其转换为 long 类型的 UserId,这样就拿到了当前用户登录的 Id 了。

2.1.2 获取当前请求的用户状态

这里的 PrincipalAccessor 是一个 IPrincipalAccessor 接口,在 ASP .NET Core 库当中他的实现名字叫做 AspNetCorePrincipalAccessor。其实你应该猜得到,在这个类的构造函数当中,注入了 HttpContext 的访问器对象 IHttpContextAccessor,这样 IAbpSession 就可以轻而易举地获得当前请求上下文当中的具体数据了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class AspNetCorePrincipalAccessor : DefaultPrincipalAccessor
{
    public override ClaimsPrincipal Principal => _httpContextAccessor.HttpContext?.User ?? base.Principal;

    private readonly IHttpContextAccessor _httpContextAccessor;

    public AspNetCorePrincipalAccessor(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }
}

2.1.3 小结

所以,Abp 通过 IAbpSession 可以轻松地知道我们当前用户的状态,包括用户 Id 与租户 Id,它只需要知道这两个东西,就可以很简单的在 IFeatureCheckerIPermissionChecker 当中来查询用户所绑定的权限来进行验证。

2.2 功能(Feature)

首先我们的思绪回到上一章所讲的 AuthorizationHelper 类,在其 AuthorizeAsync() 方法当中,使用 IFeatureChecker 来检测用户是否拥有某种功能。

1
2
3
4
5
6
7
public virtual async Task AuthorizeAsync(MethodInfo methodInfo, Type type)
{
    // 检测功能
    await CheckFeatures(methodInfo, type);
    // 检测权限
    await CheckPermissions(methodInfo, type);
}

然后呢,在 IFeatureChecker.CheckFeatures() 方法的内部,跟 IPermissionChecker 的套路一样,这里仍然是一个扩展方法,遍历方法/类上标记的 [RequiresFeatureAttribute] 特性,调用 IFeatureCheckerGetValueAsync() 方法传入功能的名称,然后将其值与 "true" 相比较,为真则是启用了该功能,其他值则说明没有启用。

1
2
3
4
5
public static async Task<bool> IsEnabledAsync(this IFeatureChecker featureChecker, string featureName)
{
    // 检查是否启用
    return string.Equals(await featureChecker.GetValueAsync(featureName), "true", StringComparison.OrdinalIgnoreCase);
}

IFeatureChecker 的定义:

1
2
3
4
5
6
7
8
public interface IFeatureChecker
{
    // 传入功能名字,获取真这对于当前租户其默认值
    Task<string> GetValueAsync(string name);

    // 传入租户 Id 与功能名字,获取针对于指定 Id 租户的默认值
    Task<string> GetValueAsync(int tenantId, string name);
}

到这一步我们仍然是跟 IFeatureChecker 打交道,那么他的具体实现是怎样的呢?

先来看一下这个 IFeatureChecker 的依赖关系图:

1534171425841

目前看起来还是比较简单,他拥有一个默认实现 FeatureChecker ,其中 IFeatureValueStore 从名字就可以知道它是用来存储功能列表的,而 IFeatureManager 则是用来管理这些功能的,Feature 则是这些功能的定义。

结合之前在 IsEnabledAsync() 方法的调用,可以看到它先进入的 GetValueAsync(string name) 方法,判断当前用户的租户 Id 是否有值,如果没有值则直接抛出异常,中断权限验证。如果有值得话,传入当前登录用户的租户 Id ,从 IFeatureManager 当中获取到定义的权限,之后呢从 IFeatureValueStore 当中拿到功能具体的值,因为功能是针对租户而言的,所以一个功能针对于多个租户的值肯定是不同的,所以在这里查询具体值的时候需要传入租户 Id。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class FeatureChecker : IFeatureChecker, ITransientDependency
{
    public IAbpSession AbpSession { get; set; }

    public IFeatureValueStore FeatureValueStore { get; set; }

    private readonly IFeatureManager _featureManager;

    public FeatureChecker(IFeatureManager featureManager)
    {
        _featureManager = featureManager;

        FeatureValueStore = NullFeatureValueStore.Instance;
        AbpSession = NullAbpSession.Instance;
    }

    public Task<string> GetValueAsync(string name)
    {
    	// 判断当前登录的用户是否拥有租户 ID
        if (!AbpSession.TenantId.HasValue)
        {
            throw new AbpException("FeatureChecker can not get a feature value by name. TenantId is not set in the IAbpSession!");
        }

	   // 传入当前登录用户的租户 Id ,获取其值
        return GetValueAsync(AbpSession.TenantId.Value, name);
    }

    public async Task<string> GetValueAsync(int tenantId, string name)
    {
    	// 从功能管理器根据名字查询用户定义的功能
        var feature = _featureManager.Get(name);

	    // 获得功能的值,如果没有值则返回其默认值
        var value = await FeatureValueStore.GetValueOrNullAsync(tenantId, feature);
        if (value == null)
        {
            return feature.DefaultValue;
        }

        return value;
    }
}

聪明的你肯定猜到功能其实是用户在代码当中定义的,而功能的值则是存放在数据库当中,每个租户其值都是不一样的。这是不是让你想到了系列文章《[Abp 源码分析]五、系统设置》 SettingProvider 的实现呢?

So,这里的 IFeatureStore 的默认实现肯定是从数据库进行配置咯~

1534173553986

2.2.1 功能的定义

首先功能、权限都是树形结构,他们都可以拥有自己的子节点,这样可以直接实现针对父节点赋值而拥有其子节点的所有权限。这里先来看一下功能的的基本定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class Feature
{
    // 附加数据的一个索引器
    public object this[string key]
    {
        get => Attributes.GetOrDefault(key);
        set => Attributes[key] = value;
    }

    // 功能的附加数据
    public IDictionary<string, object> Attributes { get; private set; }

    // 父级功能
    public Feature Parent { get; private set; }

    // 功能的名称
    public string Name { get; private set; }

    // 功能的展示名称,这是一个本地化字符串
    public ILocalizableString DisplayName { get; set; }

    // 功能的描述,一样的是一个本地化字符串
    public ILocalizableString Description { get; set; }
    
    // 功能的输入类型
    public IInputType InputType { get; set; }

    // 功能的默认值
    public string DefaultValue { get; set; }

    // 功能所适用的范围
    public FeatureScopes Scope { get; set; }

    // 如果当前功能的子节点的不可变集合
    public IReadOnlyList<Feature> Children => _children.ToImmutableList();

    private readonly List<Feature> _children;

    public Feature(string name, string defaultValue, ILocalizableString displayName = null, ILocalizableString description = null, FeatureScopes scope = FeatureScopes.All, IInputType inputType = null)
    {
        Name = name ?? throw new ArgumentNullException("name");
        DisplayName = displayName;
        Description = description;
        Scope = scope;
        DefaultValue = defaultValue;
        InputType = inputType ?? new CheckboxInputType();

        _children = new List<Feature>();
        Attributes = new Dictionary<string, object>();
    }

    public Feature CreateChildFeature(string name, string defaultValue, ILocalizableString displayName = null, ILocalizableString description = null, FeatureScopes scope = FeatureScopes.All, IInputType inputType = null)
    {
        var feature = new Feature(name, defaultValue, displayName, description, scope, inputType) { Parent = this };
        _children.Add(feature);
        return feature;
    }

    public override string ToString()
    {
        return string.Format("[Feature: {0}]", Name);
    }
}

这玩意儿光看着头还是有点疼的,其实就是关于功能的基础定义,他为啥附带了一个附加描述字典,因为可以存储一些额外的信息,比如说一个短信功能,他的配额和到期时间,至于他的 Scope 则说明了它的生效范围。

2.2.2 功能管理器

接着看看 GetValueAsync(int tenantId, string name) 方法的第一句:

1
var feature = _featureManager.Get(name);

emmm,我要从 IFeatureManager 根据权限名称取得一个具体的 Feature 对象,那我们继续来看一下 IFeatureManager 接口。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public interface IFeatureManager
{
    // 根据名称获得一个具体的功能,这个名称应该是唯一的
    Feature Get(string name);

    // 根据一个名称获得一个具体的功能,如果没找到则返回 NULL
    Feature GetOrNull(string name);

    // 获得所有定义的功能
    IReadOnlyList<Feature> GetAll();
}

2.2.3 功能管理器实现

在看具体实现的时候,我们先不慌,先看一下它实现类所继承的东西。

1
internal class FeatureManager : FeatureDefinitionContextBase, IFeatureManager, ISingletonDependency

git2

WTF,他又继承了什么奇奇怪怪的东西。我们又在此来到 FeatureDefinitionContextBase ,经过一番探查总算知道这玩意儿实现自 IFeatureDefinitionContext,看看他的定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 功能定义上下文,主要功能是提供给 FeatureProvider 来创建功能的
public interface IFeatureDefinitionContext
{
    // 创建一个功能
    Feature Create(string name, string defaultValue, ILocalizableString displayName = null, ILocalizableString description = null, FeatureScopes scope = FeatureScopes.All, IInputType inputType = null);

    // 根据名称获得一个功能
    Feature GetOrNull(string name);

    // 移除一个功能
    void Remove(string name);
}

所以,你要把这些功能存放在哪些地方呢?

其实看到这个玩意儿 name-value,答案呼之欲出,其实现内部肯定是用的一个字典来存储数据的。

接着我们来到了 FeatureDefinitionContextBase 的默认实现 FeatureDefinitionContextBase,然后发现里面也是别有洞天,Abp 又把字典再次封装了一遍,这次字典的名字叫做 FeatureDictionary,你只需要记住他只提供了一个作用,就是将字典内部的所有功能项与其子功能项按照平级关系存放在字典当中。

除了内部封装了一个字典之外,在这个上下文当中,实现了创建,获取,和移除功能的方法,然后就没有了。我们再次回到功能管理器,

功能管理器集成了这个上下文基类,集合之前 IFeatureManager 所定义的接口,它就具备了随时可以修改功能集的权力。那么这些功能是什么时候被定义的,而又是什么时候被初始化到这个字典的呢?

在前面我们已经说过,Feature 的增加与之前文章所讲的系统设置是一样的,他们都是通过集成一个 Provider ,然后在模块预加载的时候,通过一个 IFeatureConfiguration 的东西被添加到 Abp 系统当中的。所以在 FeatureManager 内部注入了 IFeatureConfiguration 用来拿到用户在模块加载时所配置的功能项集合。

1
2
3
4
5
6
7
public interface IFeatureConfiguration
{
    /// <summary>
    /// Used to add/remove <see cref="FeatureProvider"/>s.
    /// </summary>
    ITypeList<FeatureProvider> Providers { get; }
}

下面给你演示一下如何添加一个功能项:

1
2
3
4
5
6
7
8
9
public class AppFeatureProvider : FeatureProvider
{
    public override void SetFeatures(IFeatureDefinitionContext context)
    {
        var sampleBooleanFeature = context.Create("SampleBooleanFeature", defaultValue: "false");
        sampleBooleanFeature.CreateChildFeature("SampleNumericFeature", defaultValue: "10");
        context.Create("SampleSelectionFeature", defaultValue: "B");
    }
}

不用猜测 FeatureProvier 的实现了,他就是一个抽象类,定义了一个 SetFeatures 方法好让你实现而已。

之后我又在模块的预加载方法吧 AppFeatureProvider 添加到了IFeatureConfiguration 里面:

1
2
3
4
5
6
7
public class XXXModule : AbpModule
{
	public override void PreInitialize()
    {
    	Configuration.Features.Providers.Add<AppFeatureProvider>();
    }
}

而功能管理器则是在 Abp 核心模块 AbpKernalModule 初始化的时候,跟着权限管理器和系统设置管理器,一起被初始化了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public override void PostInitialize()
{
    RegisterMissingComponents();

    // 这里是系统的设置的管理器
    IocManager.Resolve<SettingDefinitionManager>().Initialize();
    // 功能管理器在这里
    IocManager.Resolve<FeatureManager>().Initialize();
    // 权限管理器
    IocManager.Resolve<PermissionManager>().Initialize();
    IocManager.Resolve<LocalizationManager>().Initialize();
    IocManager.Resolve<NotificationDefinitionManager>().Initialize();
    IocManager.Resolve<NavigationManager>().Initialize();

    if (Configuration.BackgroundJobs.IsJobExecutionEnabled)
    {
        var workerManager = IocManager.Resolve<IBackgroundWorkerManager>();
        workerManager.Start();
        workerManager.Add(IocManager.Resolve<IBackgroundJobManager>());
    }
}

看看功能管理器的定义就知道了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public void Initialize()
{
    foreach (var providerType in _featureConfiguration.Providers)
    {
        using (var provider = CreateProvider(providerType))
        {
            provider.Object.SetFeatures(this);
        }
    }

    Features.AddAllFeatures();
}

波澜不惊的我早已看透一切,可以看到这里他通过遍历注入的 FeatureProvider 集合,传入自己,让他们可以向自己注入定义的功能项。

2.2.4 功能的存储

继续看 IFeatureChecker 的代码,最后从功能管理器拿到了功能项之后,就要根据租户的 Id 取得它具体的值了。值还能存在哪儿,除了数据库最合适放这种东西,其他的你愿意也可以存在 TXT 里面。

1
2
3
4
5
public interface IFeatureValueStore
{
    // 很简洁,你传入当前用户的租户 Id 与 当前需要校验的功能项,我给你他的值
    Task<string> GetValueOrNullAsync(int tenantId, Feature feature);
}

废话不多说,来到 Zero 关于这个功能存储类的定义 AbpFeatureValueStore<TTenant,TUser>,你先不着急看那两个泛型参数,这两个泛型就是你的用户与租户实体,我们先看看这玩意儿继承了啥东西:

1
2
3
4
5
6
7
8
public class AbpFeatureValueStore<TTenant, TUser> :
        IAbpZeroFeatureValueStore,
        ITransientDependency,
        IEventHandler<EntityChangedEventData<Edition>>,
        IEventHandler<EntityChangedEventData<EditionFeatureSetting>>

        where TTenant : AbpTenant<TUser>
        where TUser : AbpUserBase

可以看到它首先继承了 IAbpZeroFeatureValueStore 接口,这里的 IAbpZeroFeatureValueStore 接口一样的继承的 IFeatureValueStore,所以在 Abp 底层框架能够直接使用。

其次我们还看到它监听了两个实体变更事件,也就是 Edition 与 EditFeatureSettings 表产生变化的时候,会进入到本类进行处理,其实这里的处理就是发生改变之后,拿到改变实体的 Id,从缓存清除掉脏数据而已。

然后我们直奔主题,找到方法的实现:

1
2
3
4
public virtual Task<string> GetValueOrNullAsync(int tenantId, Feature feature)
{
    return GetValueOrNullAsync(tenantId, feature.Name);
}

发现又是一个空壳子,继续跳转:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public virtual async Task<string> GetValueOrNullAsync(int tenantId, string featureName)
{
    // 首先从租户功能值表获取功能的值
    var cacheItem = await GetTenantFeatureCacheItemAsync(tenantId);
    // 获得到值
    var value = cacheItem.FeatureValues.GetOrDefault(featureName);
    // 不等于空,优先获取租户的值而忽略掉版本的值
    if (value != null)
    {
        return value;
    }

    // 如果租户功能值表的缓存说我还有版本 Id,那么就去版本级别的功能值表查找功能的值
    if (cacheItem.EditionId.HasValue)
    {
        value = await GetEditionValueOrNullAsync(cacheItem.EditionId.Value, featureName);
        if (value != null)
        {
            return value;
        }
    }

    return null;
}

这才是真正的获取功能值的地方,其余方法就不再详细讲述,这两个从缓存获取的方法,都分别有一个工厂方法从数据库拿去数据的,所以你也不用担心缓存里面不存在值的情况。

2.2.5 小结

总的来说功能是针对租户的一个权限,Abp 建议一个父母功能一般定义为 布尔功能。只有父母功能可用时,子功能才可用。ABP不强制这样做,但是建议这样做。

在一个基于 Abp 框架的系统功能权限是可选的,具体使用还是取决于你所开发的业务系统是否有这种需求。

2.3 权限(Permission)

2.3.1 权限的定义

权限的定义与 Feature 一样,都是存放了一些基本信息,比如说权限的唯一标识,权限的展示名称与描述,只不过少了 Feature 的附加属性而已。下面我们就会加快进度来说明一下权限相关的知识。

2.3.2 权限检测器

权限相比于功能,权限更加细化到了用户与角色,角色通过与权限关联,角色就是一个权限组的集合,用户再跟角色进行关联。看看权限管理器的定义吧:

1
2
3
public abstract class PermissionChecker<TRole, TUser> : IPermissionChecker, ITransientDependency, IIocManagerAccessor
        where TRole : AbpRole<TUser>, new()
        where TUser : AbpUser<TUser>

还是相对而言比较简单的,在这里你只需要关注两个东西:

1
2
3
4
5
6
7
8
9
public virtual async Task<bool> IsGrantedAsync(string permissionName)
{
    return AbpSession.UserId.HasValue && await _userManager.IsGrantedAsync(AbpSession.UserId.Value, permissionName);
}

public virtual async Task<bool> IsGrantedAsync(long userId, string permissionName)
{
    return await _userManager.IsGrantedAsync(userId, permissionName);
}

这就是权限校验的实现,第一个是传入当前用户的 Id 扔到 _userManager 进行校验,而第二个则扔一个用户制定的 Id 进行校验。

看到这里,我们又该到下一节了,讲解一下这个 _userManager 是何方神圣。

2.3.3 用户管理器

如果读者接触过 ASP.NET Core MVC 的 Identity 肯定对于 UserManager<,> 不会陌生,没错,这里的 _userManager 就是继承自 UserManager<TUser, long>, 实现的 AbpUserManager<TRole, TUser>

继续我们还是看关键方法 IsGrantedAsync()

1
2
3
4
5
6
7
8
public virtual async Task<bool> IsGrantedAsync(long userId, string permissionName)
{
    // 传入用户 ID 与需要检测的权限,通过权限管理器获得 Permission 对象
    return await IsGrantedAsync(
        userId,
        _permissionManager.GetPermission(permissionName)
    );
}

还是个空壳子,继续跳转:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public virtual async Task<bool> IsGrantedAsync(long userId, Permission permission)
{
    // 首先检测当前用户是否拥有租户信息
    if (!permission.MultiTenancySides.HasFlag(GetCurrentMultiTenancySide()))
    {
        return false;
    }

    // 然后检测权限依赖的功能,如果功能没有启用,一样的是没权限的
    if (permission.FeatureDependency != null && GetCurrentMultiTenancySide() == MultiTenancySides.Tenant)
    {
        FeatureDependencyContext.TenantId = GetCurrentTenantId();

        if (!await permission.FeatureDependency.IsSatisfiedAsync(FeatureDependencyContext))
        {
            return false;
        }
    }

    // 获得当前用户所拥有的权限,没有权限一样滚蛋
    var cacheItem = await GetUserPermissionCacheItemAsync(userId);
    if (cacheItem == null)
    {
        return false;
    }

    // 检测当前用户是否被授予了特许权限,没有的话则直接跳过,有的话说明这是个特权用户,拥有这个特殊权限
    if (cacheItem.GrantedPermissions.Contains(permission.Name))
    {
        return true;
    }

    // 检测禁用权限名单中是否拥有本权限,如果有,一样的不通过
    if (cacheItem.ProhibitedPermissions.Contains(permission.Name))
    {
        return false;
    }

    // 检测用户角色是否拥有改权限
    foreach (var roleId in cacheItem.RoleIds)
    {
        if (await RoleManager.IsGrantedAsync(roleId, permission))
        {
            return true;
        }
    }

    return false;
}

这里我们没有讲解权限管理器与权限的注入是因为他们两个简直一毛一样好吧,你可以看看权限的定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class MyAuthorizationProvider : AuthorizationProvider
{
    public override void SetPermissions(IPermissionDefinitionContext context)
    {
        var administration = context.CreatePermission("Administration");

        var userManagement = administration.CreateChildPermission("Administration.UserManagement");
        userManagement.CreateChildPermission("Administration.UserManagement.CreateUser");

        var roleManagement = administration.CreateChildPermission("Administration.RoleManagement");
    }
}

是不是感觉跟功能的 Provider 很像…

1534180080642

2.3.4 小结

权限仅仅会与用于和角色挂钩,与租户无关,它和功能的实现大同小异,但是也是值得我们借鉴学习的。

3.多租户数据过滤

租户与租户之间是如何进行数据过滤的呢?

这里简单讲一下单部署-单数据库的做法吧,在 EF Core 当中针对每一个实体都提供了一个全局过滤的方法 HasQueryFilter,有了这个东西,在每次 EF Core 进行查询的时候都会将查询表达式附加上你自定义的过滤器一起进行查询。

在 Abp 内部定义了一个借口,叫做 IMustHaveTenant,这玩意儿有一个必须实现的属性 TenantId,所以只要在你的实体继承了该接口,肯定就是会有 TenantId 字段咯,那么 Abp 就可以先判断你当前的实体是否实现了 IMusHaveTenant 接口,如果有的话,就给你创建了一个过滤器拼接到你的查询表达式当中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // DbContext 模型创建的时候
    base.OnModelCreating(modelBuilder);

    // 遍历所有 DbContext 定义的实体
    foreach (var entityType in modelBuilder.Model.GetEntityTypes())
    {
        ConfigureGlobalFiltersMethodInfo
            .MakeGenericMethod(entityType.ClrType)
            .Invoke(this, new object[] { modelBuilder, entityType });
    }
}

protected void ConfigureGlobalFilters<TEntity>(ModelBuilder modelBuilder, IMutableEntityType entityType)
where TEntity : class
{
    // 判断实体是否实现了租户或者软删除接口,实现了则添加一个过滤器
    if (entityType.BaseType == null && ShouldFilterEntity<TEntity>(entityType))
    {
        var filterExpression = CreateFilterExpression<TEntity>();
        if (filterExpression != null)
        {
            modelBuilder.Entity<TEntity>().HasQueryFilter(filterExpression);
        }
    }
}

// 数据过滤用的查询表达式构建
protected virtual Expression<Func<TEntity, bool>> CreateFilterExpression<TEntity>()
    where TEntity : class
{
    Expression<Func<TEntity, bool>> expression = null;

    if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
    {
        /* This condition should normally be defined as below:
            * !IsSoftDeleteFilterEnabled || !((ISoftDelete) e).IsDeleted
            * But this causes a problem with EF Core (see https://github.com/aspnet/EntityFrameworkCore/issues/9502)
            * So, we made a workaround to make it working. It works same as above.
            */
        Expression<Func<TEntity, bool>> softDeleteFilter = e => !((ISoftDelete)e).IsDeleted || ((ISoftDelete)e).IsDeleted != IsSoftDeleteFilterEnabled;
        expression = expression == null ? softDeleteFilter : CombineExpressions(expression, softDeleteFilter);
    }

    if (typeof(IMayHaveTenant).IsAssignableFrom(typeof(TEntity)))
    {
        /* This condition should normally be defined as below:
            * !IsMayHaveTenantFilterEnabled || ((IMayHaveTenant)e).TenantId == CurrentTenantId
            * But this causes a problem with EF Core (see https://github.com/aspnet/EntityFrameworkCore/issues/9502)
            * So, we made a workaround to make it working. It works same as above.
            */
        Expression<Func<TEntity, bool>> mayHaveTenantFilter = e => ((IMayHaveTenant)e).TenantId == CurrentTenantId || (((IMayHaveTenant)e).TenantId == CurrentTenantId) == IsMayHaveTenantFilterEnabled;
        expression = expression == null ? mayHaveTenantFilter : CombineExpressions(expression, mayHaveTenantFilter);
    }

    if (typeof(IMustHaveTenant).IsAssignableFrom(typeof(TEntity)))
    {
        /* This condition should normally be defined as below:
            * !IsMustHaveTenantFilterEnabled || ((IMustHaveTenant)e).TenantId == CurrentTenantId
            * But this causes a problem with EF Core (see https://github.com/aspnet/EntityFrameworkCore/issues/9502)
            * So, we made a workaround to make it working. It works same as above.
            */
        Expression<Func<TEntity, bool>> mustHaveTenantFilter = e => ((IMustHaveTenant)e).TenantId == CurrentTenantId || (((IMustHaveTenant)e).TenantId == CurrentTenantId) == IsMustHaveTenantFilterEnabled;
        expression = expression == null ? mustHaveTenantFilter : CombineExpressions(expression, mustHaveTenantFilter);
    }

    return expression;
}

上面就是实现了,你每次使用 EF Core 查询某个表的实体都会应用这个过滤表达式。

3.1 禁用过滤

但是可以看到在创建表达式的时候这里还有一些诸如 IsSoftDeleteFilterEnabled 的东西,这个就是用于你在某些时候需要禁用掉软删除过滤器的时候所需要用到的。

看看是哪儿来的:

1
protected virtual bool IsSoftDeleteFilterEnabled => CurrentUnitOfWorkProvider?.Current?.IsFilterEnabled(AbpDataFilters.SoftDelete) == true;

可以看到这个玩意儿是使用当前的工作单元来进行控制的,检测当前工作单元的过滤器是否被启用,如果实体被打了软删除接口,并且被启用的话,那么就执行过滤,反之亦然。

这些过滤器都是放在 AbpDataFilters 当中的,现在有以下几种定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public static class AbpDataFilters
{
    public const string SoftDelete = "SoftDelete";

    public const string MustHaveTenant = "MustHaveTenant";

    public const string MayHaveTenant = "MayHaveTenant";

    public static class Parameters
    {
        public const string TenantId = "tenantId";
    }
}

而这些过滤器是在 AbpKernelModule 的预加载方法当中被添加到 UOW 的默认配置当中的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public override void PreInitialize()
{
    // ... 其他代码
    AddUnitOfWorkFilters();
    // ... 其他代码
}

private void AddUnitOfWorkFilters()
{
    Configuration.UnitOfWork.RegisterFilter(AbpDataFilters.SoftDelete, true);
    Configuration.UnitOfWork.RegisterFilter(AbpDataFilters.MustHaveTenant, true);
    Configuration.UnitOfWork.RegisterFilter(AbpDataFilters.MayHaveTenant, true);
}

这些东西被添加到了 IUnitOfWorkDefaultOptions 之后,每次初始化一个工作单元,其自带的 Filiters 都是从这个 IUnitOfWorkDefaultOptions 拿到的,除非用户显式指定 UowOptions 配置。

TIM–20180814084631

Built with Hugo
主题 StackJimmy 设计