新文章 网摘 文章 随笔 日记

在ASP.NET Core中处理授权的更好方法(自定义授权)

第1部分:在ASP.NET Core中处理授权的更好方法

上次更新时间:2019年9月30日| 创建于:2018年12月14日

我的一位客户要求我帮助构建一个相当大的Web应用程序,它们的身份验证 (即检查谁在登录)和授权 (即已登录的用户可以访问哪些页面/功能)非常复杂。根据我的经验,知道使用ASP.NET的基于角色的方法并不能解决问题,我发现新的基于ASP.NET Core策略的方法确实很聪明,但是需要我编写很多(枯燥的)策略。

最后,我为客户创建了一个解决方案,并且本文介绍了授权部分–我将其称为“角色到权限”(在阅读本文时,名称会更有意义)。我还构建了一个示例ASP.NET Core应用程序,其中包含所有支持本文的新代码。当我使用ASP.NET Core内置身份系统(客户端系统需要OAuth2)时,此示例应用程序与客户端系统完全不同。该示例应用程序包含大约60行,我从原始实现中复制了这些行(经客户许可),以创建您可以使用的开源版本(MIT许可证)。

本文是ASP.NET Core中有关授权的系列文章的一部分

更新:本系列中添加了新文章

本文引起了人们的极大兴趣,因此我写了一些后续文章,回答了前两篇文章中的一些评论/问题。如果要添加本文中描述的功能,请使用仓库PermissionAccessControl2中新的,经过改进的新示例应用程序,并查看第7部分中的文章

注意:您可以克隆GitHub存储库并在本地运行–它使用内存数据库,因此可以在任何地方运行。该应用程序是使用ASP.NET Core 2.1编写的。

这是一篇很长的文章,因此这里链接到主要部分:

TL; DR; –摘要

  • 基于ASP.NET角色的授权系统适用于具有简单授权规则的系统,但是它具有局限性,例如,如果更改授权规则,则必须重新发布代码。
  • 角色,包含“管理器”或“ExternalBuyer”的名称,使用户感觉(人或外部服务),因为它们定义的用户应该能够做什么“用例”。
  • 但是,将角色应用于ASP.NET Core动作或Razor页面时,它们不能很好地工作。在这里,您需要一个更细粒度的解决方案,其名称应为“ CanRequestHoliday”,“ CanApproveHoliday” –我称之为“ 权限”
  • 解决方案是将用户的角色映射到一组权限,并将其存储在用户的声明中。
  • 然后,我使用ASP.NET Core的新的基于策略的授权系统来检查“用户的权限声明”是否包含放置在他们要访问的操作/页面上的权限。
  • 一个开源例如ASP.NET核心应用去这篇文章。

设置场景–了解不同的应用程序安全需求

如果您了解ASP.NET的授权和身份验证功能,则可以跳过本节。

有数十亿个Web应用程序,对您可以做的事情的控制范围涵盖了“任何人都可以做的事”,例如Google搜索,某些军事系统,其中访问需要密钥,生物识别等。当您需要证明自己是有效用户时系统的身份验证(例如通过登录)称为身份验证登录后,您可以执行的操作由所谓的授权控制

授权分为两部分:

  1. 我可以访问哪些数据?例如,您可以看到您的个人信息,但看不到其他人的个人信息。
  2. 您可以使用哪些功能?例如,您是否可以更改可以看到的书名?

注意:本文介绍了一种管理第二部分的方法,可以使用哪些功能。

ASP.NET MVC和现在的ASP.NET Core具有各种系统来帮助进行授权和身份验证。某些系统仅需要简单的授权-我可以想象一个非常简单的电子商务系统可以摆脱:a)无需登录-浏览,b)登录-购买和c)管理员-添加/删除待售商品。可以使用ASP.NET基于角色的身份验证来完成。

但是,许多企业对企业(企业对企业)系统具有更复杂的授权需求。例如,考虑一个人力资源(HR)系统,人们可以在该系统上申请休假,而经理必须批准这些请求–内部正在做很多事情,以确保只有正确的用户才能使用这些功能。

像我的示例HR系统这样的系统通常最终都有许多复杂的授权规则。我的经验是,基于ASP.NET角色的身份验证开始在实现这种类型的系统时遇到问题,这就是为什么我创建了“角色到权限”代码的原因。

可以从“角色到权限”方法中受益的另一种应用程序是订阅系统,用户可以访问的功能取决于他们所支付的订阅费用。角色到权限方法可以控制用户可以根据购买的订阅访问的功能。

角色授权:这是什么,它有什么限制?

在ASP.NET MVC应用程序中,角色授权已经存在多年了,我已经在许多应用程序中使用了它。这是一个ASP.NET Core控制器的示例,只有具有“ Staff”角色或“ Manger”角色的登录用户才能访问该控制器。

1个
2
3
4
5
[Authorize(Roles = "Staff,Manager")]
public ActionResult Index()
{
    return View(MyData);
}

这适用于具有相当简单且定义明确的角色的应用程序,例如User / Admin或Staff / Manager / Admin,那么Roles是一个不错的选择。但是这是我发现的一些问题:

  1. 如果您有很多角色,则可以使用较长的Authorize属性,例如[Authorize(Roles =“ Staff,HrManager,BizManager,DevManage,Admin,SuperAdmin”))。
  2. 因为Authorize是属性,所以字符串必须是常量,例如,您不能有$” {RoleConstants.StaffRole}”。这意味着,如果情况发生变化,则您正在编辑字符串,并且您可能会很容易拼错某些内容。
  3. 对我而言,最大的好处是您的授权规则已硬编码到您的代码中。因此,如果要更改可以访问特定操作的人员,则必须编辑适当的Authorize属性并重新部署应用程序。

我从以前使用基于角色的授权的应用程序中获得的经验是,在开发或完善应用程序时,我经常不得不回去编辑授权角色部分。一段时间以来,我一直在寻找一种更好的方法,并且我客户的要求促使我去寻找比“角色授权”更好的东西。

角色到权限系统的体系结构

1.介绍角色,权限和模块

事实证明,拥有角色的用户的想法没有错。用户(人员或外部服务)通常可以通过其职能或部门来描述,例如“开发人员”或“ HR”,可能还具有“ HolidayAdmin”之类的辅助角色。将角色视为“ 用户用例 ” 

注意:在示例应用程序中,我的角色为“工作人员”,“经理”和“管理员”。

但是问题是角色不适合控制器中的动作。每个Controller动作在角色中扮演一小部分,或者扭转角色,角色由一系列允许您访问的Controller动作组成。我决定将每个动作的授权功能称为“权限”,并使用枚举定义它们。权限枚举成员可以称为“ CanReadHoliday”,“ CanRequestHoliday”,“ CanApproveHoliday”等。

注意:在示例应用程序中,我的ColorController拥有“ ColorRead”,“ ColorCreate”,“ ColorUpdate”和“ ColorDelete”的权限。

现在我们有了权限,我们可以提供另一个功能,该功能可以控制对可选功能的访问,例如,用户仅基于对服务的订阅才能拥有这些功能。有很多处理功能的方法,但是通过将可选功能组合到权限中,可以更轻松地设置和控制。

注意:在示例应用程序中,我具有名为“ Feature1”和“ Feature2”的权限,它们映射到具有相同名称的模块。

2.如何实施

定义好术语后,我将为您提供整个过程的概述。它由两部分组成:登录阶段和对网站的正常访问。登录阶段是最复杂的,后台会进行很多魔术操作。它的基本工作是将用户的角色转换为权限,并将其添加到用户的信息中。

我已经建立了示例应用程序,将用户的声明存储在cookie中,该cookie在每个HTTP请求中都读取并转换为ClaimsPrincipal,可以在ASP.NET Core中通过称为“ User”的HttpContext属性对其进行访问。

这是该登录阶段的示意图。这可能还没有多大意义,但是我将在本文的其余部分中介绍每个部分。此图是给您一个概述。

更新:在“ 第3部分:处理ASP.NET Core授权的更好方法–六个月 ”中,我展示了一种通过Roles-to-Permissions数据库处理Roles的方法。这使得该方法与其他身份验证方法(如社交媒体,AzureAd等)更加有用。

第二部分更简单,介绍了每次登录的用户访问受保护的Controller操作时发生的情况。基本上,我具有带有动态规则的基于策略的授权,该授权可以检查当前用户是否具有执行ASP.NET操作/剃须刀页面所需的权限。

注意:如果您想查看实际的代码,请不要忘记有示例应用程序

现在,我将分阶段构建“角色到权限”,并解释每个部分的作用。

为什么我使用Enums作为权限

使用角色的缺点之一是它使用了字符串,而且我有点诵读困难。这意味着我可以错误地键入/拼写东西而不会注意到。因此,我想要的是能提示我的东西,如果我仍然输入不正确,那将是编译错误。但是事实证明,还有其他一些原因使得将枚举用于权限是一个好主意。让我解释。

在大型应用程序中,可能有数百个权限。这导致两个问题:

  1. 如果我使用Cookie授权,则Cookie的最大大小为4096字节。如果我有数百个长字符串,我可能会开始填写Cookie,并且我还需要一些空间来处理其他事情,例如我的数据授权。如果我可以将Enums权限存储为一系列整数,则它将比一系列字符串小得多。
  2. 其次,我想帮助需要构建从角色到权限的映射的管理员。如果他们需要滚动浏览数百个权限名称,可能很难确定需要哪些权限名称。事实证明,枚举成员可以具有属性,因此我可以添加其他信息来帮助管理员。

所以,这是我的权限枚举代码的一部分

1个
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18岁
19
20
21
22
public enum Permissions
{
    //Here is an example of very detailed control over something
    [Display(GroupName = "Color", Name = "Read", Description = "Can read colors")]
    ColorRead = 0x10,
    [Display(GroupName = "Color", Name = "Create", Description = "Can create a color entry")]
    ColorCreate = 0x11,
    [Display(GroupName = "Color", Name = "Update", Description = "Can update a color entry")]
    ColorUpdate = 0x12,
    [Display(GroupName = "Color", Name = "Delete", Description = "Can delete a color entry")]
    ColorDelete = 0x13,
 
    [Display(GroupName = "UserAdmin", Name = "Read users", Description = "Can list User")]
    UserRead = 0x20,
    //This is an example of grouping multiple actions under one permission
    [Display(GroupName = "UserAdmin", Name = "Alter user", Description = "Can do anything to the User")]
    UserChange = 0x21,
 
    [Obsolete]
    [Display(GroupName = "Old", Name = "Not used", Description = "example of old permission"
    OldPermissionNotUsed = 0x40,
//... other code left out

需要注意的是:

  • 我展示了两种类型的权限。
    • 前四个(第4至11行)是细粒度的权限,每个操作几乎一个。
    • 接下来的两行(第13至17行)较为通用,例如,我有一个特定的“ UserRead”,但是有一个名为“ UserChange”的权限,该权限允许创建,更新,删除,锁定,更改密码等。
  • 第5、7等行。请注意,我给每个枚举一个特定的数字。如果您正在使用新版本无缝替换旧版本的24/7应用程序进行操作,则权限麻木不得更改,否则用户的Claims是错误的。这就是为什么我给每个枚举一个特定的数字。
  • 第19行,我还支持Obsolete属性,该属性将停止Permission出现在清单中。关于大量重复使用具有意外后果的许多恐怖故事。(此外,如果您尝试使用标记为“过时”的内容,则会收到警告)。
  • 第4行等。我向每个权限枚举添加一个显示属性。这具有有用的信息,我可以显示许多有用的信息来帮助正在构建角色的人。
  • 第4、6、8、10行。我对在同一位置使用的权限进行“分组”。这使管理员更容易找到他们想要的东西。我也用十六进制编号,这给了我16个组的权限(我尝试了10个,可以进行修改,所以16个更好)。

这是我的示例应用程序中某些权限的列表,该列表通过Users-> List Permissions nav下拉列表列出。

以及产生该输出的代码(完整链接到PermissionDisplay类

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
public static List<PermissionDisplay> GetPermissionsToDisplay(Type enumType)
{
    var result = new List<PermissionDisplay>();
    foreach (var permissionName in Enum.GetNames(enumType))
    {
        var member = enumType.GetMember(permissionName);
        //This allows you to obsolete a permission and it won't be shown as a
        //possible option, but is still there so you won't reuse the number
        var obsoleteAttribute = member[0].GetCustomAttribute<ObsoleteAttribute>();
        if (obsoleteAttribute != null)
            continue;
        //If there is no DisplayAttribute then the Enum is not used
        var displayAttribute = member[0].GetCustomAttribute<DisplayAttribute>();
        if (displayAttribute == null)
            continue;
 
        //Gets the optional PaidForModule that a permission can be linked to
        var moduleAttribute = member[0].GetCustomAttribute<PermissionLinkedToModuleAttribute>();
 
        var permission = (Permissions)Enum.Parse(enumType, permissionName, false);
 
        result.Add(new PermissionDisplay(displayAttribute.GroupName, displayAttribute.Name,
                displayAttribute.Description, permission, moduleAttribute?.PaidForModule.ToString()));
    }
 
    return result;
}

如何处理可选/付费功能?

我的客户提供了一个企业对企业应用程序,并计划添加客户可以订阅的新功能。解决此问题的一种方法是创建不同的角色,例如“ Manager”,“ ManagerWithFeature1”,“ ManagerWithFeature2”,或添加必须手动应用于用户的单独的功能角色。那行得通,但管理起来非常可怕,人为错误可能会引起问题。我的首选系统是将链接到付费功能的标记权限标记为基于用户的订阅对其进行过滤。

使用枚举可以很容易地将权限标记为链接到模块–我只是添加了另一个属性。这里是链接到模块的权限示例(请参见第5行)。

1个
2
3
4
5
6
7
8
9
10
11
public enum Permissions
{
    //… other Permissions removed for clarity
 
    [LinkedToModule(PaidForModules.Feature1)]
    [Display(GroupName = "Features", Name = "Feature1", Description = "Can access feature1")]
    Feature1Access = 0x30,
    [LinkedToModule(PaidForModules.Feature2)]
    [Display(GroupName = "Features", Name = "Feature2", Description = "Can access feature2")]
    Feature2Access = 0x31
}

付费模块再次由枚举表示,但其中一个标记为[Flags]属性,因为用户可以拥有多个已订阅的模块。这是我的PaidForModules枚举代码

1个
2
3
4
5
6
7
8
[Flags]
public enum PaidForModules : long
{
    None = 0,
    Feature1 = 1,
    Feature2 = 2,
    Feature3 = 4
}

注意我在枚举中添加了“:long”,这使我系统中最多可以有64个不同的模块。

发生的事情是,在确定用户应具有的权限时,将过滤掉链接到用户尚未订阅的模块的权限-我将在稍后介绍。当您使用对每个角色有意义的所有权限(包括映射到付费模块的功能)构建每个角色时,这将使角色的设置变得更加简单。然后,在登录时,系统将删除当前用户无权访问的所有权限。对于管理员而言,这更简单,对于应用程序则更安全。

如何将角色转换为权限声明?

在我的客户系统中,我们使用0Auth2身份验证,但是在本示例中,我使用ASP.NET Core IdentityRole来保存用户具有的角色。这意味着我可以使用所有ASP.NET Core内置的身份代码来设置用户和角色。但是,如何将用户的角色转换为权限声明?

更新:在后续文章中添加了两项改进:

  1. 在名为“ 第3部分:一种更好的处理ASP.NET Core授权的方法-六个月之久”的文章中,我展示了一种使用UserClaimsPrincipalFactory将Roles转换为权限声明的简便方法,请参阅本节
  2. PermissionAccessControl2存储的第二个代码版本中,我改进/简化了“ OnValidatePrincipal”调用的代码,并提供了我添加的各种其他功能的版本。有关代码,请参见文件夹“ AuthorizeSetup ”。

再次有几种方法,但是最后我在授权Cookie中利用了一个名为“ OnValidatePrincipal”的事件(这是示例应用程序启动类中各行链接)。这将调用下面的代码,但是请注意,它非常复杂,因此这里是其执行步骤的摘要:

  1. 如果“声明”已经具有“权限”声明类型,则没有任何操作可以快速返回。
  2. 然后我们从角色声明中获得用户拥有的角色
  3. 我需要访问我的数据库部分。我不能使用依赖项注入,因此我使用extraAuthDbContextOptions,这是我可以在启动时提供的单例。
  4. 然后,我获得了所有角色的所有权限,并通过Distinct删除了不必要的重复项。
  5. 然后,我过滤掉链接到用户无权访问的模块的所有权限。
  6. 然后,我添加一个包含所有允许的用户权限的权限Claim,但将其作为十六进制数字打包在一个字符串中,这样就不会占用太多空间(我使用了十六进制格式,因为它简化了调试)。
  7. 最后,我必须创建一个新的ClaimsPrincipal,并告诉ASP.NET Core替换当前的ClaimsPrincipal,并将所有重要上下文设置为true.ShouldRenew为true,这将更新Cookie,否则将对每个HTTP请求使用此复杂的(慢速)方法!
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
public async Task ValidateAsync(CookieValidatePrincipalContext context)
{
    if (context.Principal.Claims.Any(x =>
        x.Type == PermissionConstants.PackedPermissionClaimType))
        return;
 
    //No permissions in the claims so we need to add it
    //This is only happens once after the user has logged in
    var claims = new List<Claim>();
    foreach (var claim in context.Principal.Claims)
    {
        claims.Add(claim);
    }
 
    var usersRoles = context.Principal.Claims
        .Where(x => x.Type == ClaimTypes.Role)
        .Select(x => x.Value)
        .ToList();
    //I can't inject the DbContext here because that is dynamic,
    //but I can pass in the database options because that is a
    //From that I can create a valid dbContext to access the database
    using (var dbContext = new ExtraAuthorizeDbContext(_extraAuthDbContextOptions))
    {
        //This gets all the permissions, with a distinct to remove duplicates
        var permissionsForUser = await dbContext.RolesToPermissions
            .Where(x => usersRoles.Contains(x.RoleName))
            .SelectMany(x => x.PermissionsInRole)
            .Distinct()
            .ToListAsync();
 
        //we get the modules this user is allows to see
        var userModules =
            dbContext.ModulesForUsers
                .Find(context.Principal.Claims
                     .SingleOrDefault(x => x.Type == ClaimTypes.Name).Value)
                ?.AllowedPaidForModules ?? PaidForModules.None;
        //Now we remove permissions that are linked to modules that the user has no access to
        var filteredPermissions =
            from permission in permissionsForUser
            let moduleAttr = typeof(Permissions).GetMember(permission.ToString())[0]
                .GetCustomAttribute<LinkedToModuleAttribute>()
            where moduleAttr == null || userModules.HasFlag(moduleAttr.PaidForModule)
            select permission;
 
          //Now add it to the claim
          claims.Add(new Claim(PermissionConstants.PackedPermissionClaimType,
              filteredPermissions.PackPermissionsIntoString()));    }
 
    var identity = new ClaimsIdentity(claims, "Cookie");
    var newPrincipal = new ClaimsPrincipal(identity);
 
    context.ReplacePrincipal(newPrincipal);
    context.ShouldRenew = true;
}

如何将我的权限转换为基于策略的授权?

好的,我现在可以通过用户声明访问权限,但是如何将其转换为ASP.NET Core可用于授权的内容。.NET开发人员和朋友Jerrie Pelser在这里帮助了我。

当我开始这个项目时,我给Jerrie Pelser发了电子邮件,他运行ASP.NET每周新闻通讯(伟大的新闻通讯!一定要注册),因为我知道Jerrie是身份验证和授权方面的专家。他为我指出了一些体系结构方面的内容,而且我发现他自己的文章“ 使用ASP.NET Core动态创建授权策略 ”确实很有帮助。Jerris的文章向我展示了如何动态构建策略,这正是我所需要的。

我不会在这里重复Jerrie的文章(使用上面的链接),但是我将向您展示我的PermissionHandler,该Handler会在策略内用于检查当前User's Permissions声明是否存在并包含在action / Razor页面上的Permission。它使用称为ThisPermissionIsAllowed的扩展方法 进行检查。

1个
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18岁
19
20
21
public class PermissionHandler :
    AuthorizationHandler<PermissionRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        PermissionRequirement requirement)
    {
        var permissionsClaim = context.User.Claims
            .SingleOrDefault(c =>
                 c.Type == PermissionConstants.PackedPermissionClaimType);
        // If user does not have the scope claim, get out of here
        if (permissionsClaim == null)
            return Task.CompletedTask;
 
        if (permissionsClaim.Value
            .ThisPermissionIsAllowed(requirement.PermissionName))
            context.Succeed(requirement);
 
        return Task.CompletedTask;
    }
}

使基于策略的动态授权工作涉及其他两个类。这些是它们的链接:

策略是由字符串定义的,但是正如我所说,我讨厌字符串,因为我可能会犯错。因此,我创建了这个非常简单的HasPermission属性,该属性允许我应用Authorize属性,但使用Permissions Enum

1个
2
3
4
5
6
7
[AttributeUsage(AttributeTargets.Method
    | AttributeTargets.Class, Inherited = false)]
public class HasPermissionAttribute : AuthorizeAttribute
{
    public HasPermissionAttribute(Permissions permission)
       : base(permission.ToString()) { }
}

这很简单,但是这意味着我在添加权限时会很聪明。

全部放在一起

因此,我们在代码中具有Permissions,我们可以使用HasPermissionAttribute将它们应用于我们要通过授权保护的每个操作。这是我的示例应用程序中从ColorController采取的一项操作。

1个
2
3
4
5
[HasPermission(Permissions.ColorRead)]
public ActionResult Index()
{
    return View(MyData);
}

我们还需要向应用程序使用的任何数据库中添加两个表。EF核心两个实体类是:

一旦应用程序启动并运行,管理员类型的用户必须:

  1. 使用ASP.NET Core身份代码创建一些角色,例如“ Staff”,“ Manager”等。
  2. 为每个角色创建匹配的RoleToPermissions,并指定映射到每个Role的权限。

然后,对于每个新用户,管理员(或一些自动订阅代码)必须:

  • 创建用户(如果是仅邀请类型的应用程序)
  • 为该新用户添加正确的Roles和ModulesForUser。

完成后,我为您显示的所有代码都将接管。用户登录并获得权限,他们可以访问的内容由ASP.NET Core基于策略的身份验证进行管理。

我在其他地方没讲过的东西

我没有详细介绍一些内容,但以下是这些项目的链接:

  • 启动。在Startup类的此链接中,高亮显示了有关注册事物的重要部分(注意:我使用ASP.NET Core 2.1构建了该应用程序,但是我知道Identity部分在2.2中有所变化,因此对于新版本的ASP.NET Core,您可能必须更新放置在Startup类中的代码)。
  • 您完全不需要使用ASP.NET Core Identity系统-我说过客户端版本使用外部身份验证系统。您只需要创建一个Roles-to-User表,就可以为每个用户分配Roles。
  • 我没有介绍如何打包/解压缩权限。您可以在PermissionPackers中找到用于执行此操作的Extension方法
  • 您可能想在剃须刀页面中检查权限以显示/隐藏链接。我在PermissionsExtension 类中创建了一个简单方法,并在_Layout.cshtml Razor页面中使用了该方法

结论

好吧,这是一篇很长的文章,所以到最后都做得很好。我已经描述了我构建的身份验证,该身份验证可以处理复杂的身份验证规则,同时(相对)易于通过Admin人员进行理解和管理。当然,如果您有数百个权限,则不难设置初始RolesToPermissions,但是Admin有很多信息可以帮助他们。

对我来说,“角色到权限”方法解决了我在使用ASP.NET MVC角色构建的旧系统中遇到的许多问题。我必须编写更多代码,但这使a)更改授权规则更加容易,并且b)帮助管理员管理具有大量角色/权限的应用程序。我希望它能对您有所帮助,并让您想到为项目构建更好的身份验证系统的更好方法。

更新:请参阅PermissionAccessControl2存储库中的新代码和新文章。

新的代码和文章增加了更多功能,尤其是使代码更易于复制到您自己的应用程序中。请参阅名为“将“ 更好的ASP.NET Core授权”代码添加到您的应用程序中 ”的文章,该文章为您提供了有关如何将本文及后续文章中的功能添加到您自己的代码中的逐步教程。

进一步阅读

快乐的编码。PS。如果您对ASP.NET或Entity Framework感兴趣,请不要忘记注册Jerrie Pelser的ASP.NET Weekly时事通讯这是我收到的最重要的新闻通讯

 

注:以上Github中的源码还需要在DataLayer层添加如下代码才能作迁移,生产环境则注释掉这个代码:

 

    public class ExtraAuthorizeDbContextFactory : IDesignTimeDbContextFactory<ExtraAuthorizeDbContext>
    {
        private const string DefaultConnectionStringName = "Default";

        public ExtraAuthorizeDbContext CreateDbContext(string[] args)
        {
            var connectString = "server=.;database=PermissionAccess;uid=sa;password=!8110@eon;Trusted_Connection=True;MultipleActiveResultSets=true";

            var builder = new DbContextOptionsBuilder<ExtraAuthorizeDbContext>();
            builder.UseSqlServer(connectString);
            return new ExtraAuthorizeDbContext(builder.Options);
        }
    }

 

 

 

 

    public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
    {
        private const string DefaultConnectionStringName = "Default";

        public ApplicationDbContext CreateDbContext(string[] args)
        {
            var connectString = "server=.;database=PermissionAccess;uid=sa;password=!8110@eon;Trusted_Connection=True;MultipleActiveResultSets=true";

            var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
            builder.UseSqlServer(connectString);
            return new ApplicationDbContext(builder.Options);
        }
    }

 

    public class MultiTenantDbContextFactory : IDesignTimeDbContextFactory<MultiTenantDbContext>
    {
        private const string DefaultConnectionStringName = "Default";

        public MultiTenantDbContext CreateDbContext(string[] args)
        {
            var connectString = "server=.;database=PermissionAccess;uid=sa;password=!8110@eon;Trusted_Connection=True;MultipleActiveResultSets=true";

            var builder = new DbContextOptionsBuilder<MultiTenantDbContext>();
            builder.UseSqlServer(connectString);
            return new MultiTenantDbContext(builder.Options,null);
        }
    }

 

    public class PersonalDbContextFactory : IDesignTimeDbContextFactory<PersonalDbContext>
    {
        private const string DefaultConnectionStringName = "Default";

        public PersonalDbContext CreateDbContext(string[] args)
        {
            var connectString = "server=.;database=PermissionAccess;uid=sa;password=!8110@eon;Trusted_Connection=True;MultipleActiveResultSets=true";

            var builder = new DbContextOptionsBuilder<PersonalDbContext>();
            builder.UseSqlServer(connectString);
            return new PersonalDbContext(builder.Options,null);
        }
    }

Dbcontext也要作一下修改:

using System.Threading;
using System.Threading.Tasks;
using DataAuthorize;
using DataLayer.EfClasses.MultiTenantClasses;
using Microsoft.EntityFrameworkCore;

namespace DataLayer.EfCode
{
    public class MultiTenantDbContext : DbContext
    {
        public int ShopKey { get; }
        public string DistrictManagerId { get; }

        public DbSet<MultiTenantUser> MultiTenantUsers { get; set; }
        public DbSet<Shop> Shops { get; set; }
        public DbSet<StockInfo> CurrentStock { get; set; }

        public MultiTenantDbContext(DbContextOptions<MultiTenantDbContext> options, IGetClaimsProvider userData)
            : base(options)
        {
            if (userData!=null)
            {
                ShopKey = userData.ShopKey;
                DistrictManagerId = userData.DistrictManagerId;
            }
        }

        //I only have to override these two version of SaveChanges, as the other two versions call these
        public override int SaveChanges(bool acceptAllChangesOnSuccess)
        {
            this.MarkCreatedItemWithShopKey(ShopKey);
            return base.SaveChanges(acceptAllChangesOnSuccess);
        }

        public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new CancellationToken())
        {
            this.MarkCreatedItemWithShopKey(ShopKey);
            return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            //Standard multi-tenant query filter - just filter on ShopKey
            //This assumes that the district manager cannot manage the shop users
            modelBuilder.Entity<MultiTenantUser>().HasQueryFilter(x => x.ShopKey == ShopKey);

            //Altered query filter to handle hierarchical access
            modelBuilder.Entity<Shop>().HasQueryFilter(x => DistrictManagerId == null
                ? x.ShopKey == ShopKey
                : x.DistrictManagerId == DistrictManagerId);
            modelBuilder.Entity<StockInfo>().HasQueryFilter(x => DistrictManagerId == null 
                ? x.ShopKey == ShopKey
                : x.DistrictManagerId == DistrictManagerId);
        }
    }
}
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DataAuthorize;
using DataLayer.EfClasses.BusinessClasses;
using Microsoft.EntityFrameworkCore;

namespace DataLayer.EfCode
{
    public class PersonalDbContext : DbContext
    {
        private readonly string _userId;

        public DbSet<PersonalData> PersonalDatas { get; set; }

        public PersonalDbContext(DbContextOptions<PersonalDbContext> options, IGetClaimsProvider userData)
            : base(options)
        {
            if (userData!=null)
            {
                _userId = userData.UserId;
            }
        }

        //I only have to override these two version of SaveChanges, as the other two versions call these
        public override int SaveChanges(bool acceptAllChangesOnSuccess)
        {
            this.MarkCreatedItemAsOwnedBy(_userId);
            return base.SaveChanges(acceptAllChangesOnSuccess);
        }

        public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new CancellationToken())
        {
            this.MarkCreatedItemAsOwnedBy(_userId);
            return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            foreach (var entityOwnedBy in modelBuilder.Model.GetEntityTypes().Where(x => x.ClrType.GetInterface(nameof(IOwnedBy)) != null))
            {
                modelBuilder.Entity(entityOwnedBy.ClrType).HasIndex(nameof(IOwnedBy.OwnedBy));
            }

            modelBuilder.Entity<PersonalData>().HasQueryFilter(x => x.OwnedBy == _userId);
        }
    }
}

 

 

posted @ 2020-04-29 15:08  岭南春  阅读(350)  评论(0)    收藏  举报