【问题标题】:Permissions per page in ASP.NET CoreASP.NET Core 中每页的权限
【发布时间】:2019-01-12 21:02:49
【问题描述】:

我有一个角色和声明不支持的场景,所以我遵循了实现这个场景的路径,我想问一些问题并告诉我我所做的是否是正确的方式或建议不同的方式实施它。 首先,我想通过使用这样的属性来定义每个 Controller / Action 或 Razor 页面的权限:

[Data.CheckAccess(PermissionsEnum.Users_Create)]
public class PrivacyModel : PageModel
{
    public void OnGet()
    {
    }
}

PermissionsEnum 具有以下形式:

public enum PermissionsEnum
{
    Users_View = 101,
    Users_Create = 102,
    Users_Edit = 103,
    Users_Delete = 103,
    Users_Details = 104,

    Products_View = 201,
    Products_Create = 202,
    Products_Edit = 203,
    Products_Delete = 204,
    Products_Details = 205
}

我修改了 IdentityRole,以便能够为每个角色附加一个权限列表。

public class ApplicationRole : IdentityRole
{
    public string Permissions { get; set; }

    public void SetPermissions(List<PermissionsEnum> permissions)
    {
        Permissions = Newtonsoft.Json.JsonConvert.SerializeObject(permissions);
    }

    public List<PermissionsEnum> GetPermissions()
    {
        return Newtonsoft.Json.JsonConvert.DeserializeObject<List<PermissionsEnum>>(Permissions);
    }
}

EntityFramework 不能具有 List 类型的属性,因此我使用了字符串属性,并且我有两个帮助方法来序列化和反序列化枚举列表。 现在我可以创建管理页面,用户可以创建角色并检查按用户分组的权限,以及产品(这是我在枚举器中拥有的两个组),以便更轻松地管理权限。 创建角色后,我将能够使用其他页面为用户分配角色。 我创建了以下属性来进行授权:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class CheckAccessAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    private PermissionsEnum permission;

    public CheckAccessAttribute(PermissionsEnum permission)
    {
        this.permission = permission;
    }

    public async void OnAuthorization(AuthorizationFilterContext context)
    {
        if (!context.HttpContext.User.Identity.IsAuthenticated)
        {
            return;
        }

        UserManager<ApplicationUser> userManager = (UserManager<ApplicationUser>)context.HttpContext.RequestServices.GetService(typeof(UserManager<ApplicationUser>));
        var user = await userManager.GetUserAsync(context.HttpContext.User);
        RoleManager<ApplicationRole> roleManager = (RoleManager<ApplicationRole>)context.HttpContext.RequestServices.GetService(typeof(RoleManager<ApplicationRole>));

        var roles = await userManager.GetRolesAsync(user);

        foreach (var role in roles)
        {
            var CurrentRole = await roleManager.FindByNameAsync(role);

            if (CurrentRole.GetPermissions().Contains(permission))
                return;
        }

        // the user has not this permission
        context.Result = new StatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
        return;
    }
}

这很好,但是当我启动应用程序时,我登录并停止应用程序,然后我启动应用程序,用户登录,因为有一个身份验证 cookie。这将导致应用程序崩溃。错误消息是当我尝试让用户使用用户管理器时。错误信息如下。

System.ObjectDisposedException: '无法访问已释放的对象。此错误的一个常见原因是释放从依赖注入中解析的上下文,然后尝试在应用程序的其他地方使用相同的上下文实例。如果您在上下文上调用 Dispose() 或将上下文包装在 using 语句中,则可能会发生这种情况。如果你使用依赖注入,你应该让依赖注入容器负责处理上下文实例。 对象名称:'AsyncDisposer'。'

所以我改变了我的代码以绕过这个问题,现在的代码如下。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class CheckAccessAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    private PermissionsEnum permission;

    public CheckAccessAttribute(PermissionsEnum permission)
    {
        this.permission = permission;
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        if (!context.HttpContext.User.Identity.IsAuthenticated)
        {
            return;
        }

        var userId = context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;

        ApplicationDbContext DbContext = (ApplicationDbContext)context.HttpContext.RequestServices.GetService(typeof(ApplicationDbContext));

        var user = DbContext.Users.Where(m => m.Id == userId).FirstOrDefault();
        var RoleIDs = DbContext.UserRoles.Where(m => m.UserId == user.Id).Select(m => m.RoleId);
        var Roles = DbContext.Roles.Where(m => RoleIDs.Contains(m.Id)).ToList();
        foreach (var role in Roles)
        {
            if (role.GetPermissions().Contains(permission))
                return;
        }

        // the user has not this permission
        context.Result = new StatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
        return;
    }
}

我有以下问题。

  1. 有没有更好的方法来实现这个场景?
  2. 为什么会出现 System.ObjectDisposedException 异常。有没有办法解决这个问题?
  3. 为了提高性能,我会尽量缓存当前用户的权限,这样就不用每次页面使用该属性时都加载它们。我想我会使用 Cache in-memory 方法(https://docs.microsoft.com/en-us/aspnet/core/performance/caching/memory?view=aspnetcore-2.2)。这是最好的方法吗?

【问题讨论】:

    标签: asp.net-core asp.net-core-2.0 asp.net-core-2.1


    【解决方案1】:

    一些建议:

    • 请看:How do you create a custom AuthorizeAttribute in ASP.NET Core?
    • 由于您只是对每个访问受控“区域”(用户、产品...)重复操作(查看、创建、编辑、删除、详细信息),因此请考虑将这些“区域”和权限分隔在单独的文件中。然后你可以在你的属性中写[CheckAccess(user, create)] 让前端也更容易设置权限。
    • 使用HasConversion 进行权限序列化。
    • “我想我会使用 Cache in-memory 方法” - 如果您缓存权限,请务必记住每次更改用户权限或添加新权限时都要使此缓存无效。
    • Do not use async void 除非用于异步事件处理程序。这很可能是您遇到ObjectDisposedException 的原因,因为您的方法是异步执行的,并且调用进程不会等待异步操作完成并释放数据库上下文。请改用async version of the IAuthorizationFilter。看看这个答案:https://stackoverflow.com/a/53856127/2477619

    【讨论】:

    • 关于分离到区域和动作会使事情变得更加复杂,因为根据要求会有针对特定区域的额外动作。 HasConversion 是我不知道的,我将阅读它以了解它是如何工作的。我必须使用异步,因为我必须进行等待调用才能获取用户和角色。
    • 我知道您需要它是异步的,但您不能简单地将 dotnet 框架调用的同步方法强制为异步。框架对OnAuthorization 的调用不是由调用者await 调用的,而是会简单地以同步方式处理。
    • 我用一个应该澄清这一点的链接更新了我的答案stackoverflow.com/a/53855641/2477619
    • “关于分离到区域和动作会使事情变得更加复杂,因为根据要求会有针对特定区域的额外动作。”好的,这是一个有效的观点。我只想提一下,这样做的缺点是您需要创建很多“常量”(也在前端)。此外,前端不能简单地生成一个 CRUD-permission-table 来设置权限。但是使用最适合你的东西:)
    • IAuthorizedFilter 的异步版本运行良好。关于缓存,我想在接下来的 10 分钟或类似的时间内使缓存过期,但每次更改角色或权限时使所有缓存信息失效会更好。我没有缓存方面的经验,但我认为实现起来并不难。
    猜你喜欢
    • 2017-11-06
    • 2018-01-13
    • 1970-01-01
    • 1970-01-01
    • 2018-12-27
    • 1970-01-01
    • 2020-06-10
    • 1970-01-01
    • 2017-04-15
    相关资源
    最近更新 更多