多用户应用程序中的数据访问

开发多用户系统时,几乎总是出现限制访问数据的问题。 主要方案如下:

  1. 认证用户的数据访问限制
  2. 限制访问经过身份验证但不具备用户必要特权的数据
  3. 通过直接调用API防止未经授权的访问
  4. 过滤搜索查询中的数据并列出UI元素(表,列表)
  5. 防止其他用户更改属于一个用户的数据

对场景1-3进行了很好的描述,通常使用内置的框架工具(例如, 基于角色的授权或基于声明的授权)来解决。 但是,授权用户可以直接访问“邻居”的数据或在其帐户中执行操作的情况始终存在。 发生这种情况的最常见原因是程序员忘记添加必要的检查。 您可以依靠代码审查,也可以通过应用全局数据过滤规则来防止此类情况。 它们将在本文中讨论。

列表和表格


在ASP.NET MVC中用于接收数据的典型控制器可能看起来像这样

[HttpGet] public virtual IActionResult Get([FromQuery]T parameter) { var total = _dbContext .Set<TEntity>() .Where(/* some business rules */) .Count(); var items= _dbContext .Set<TEntity>() .Where(/* some business rules */) .ProjectTo<TDto>() .Skip(parameter.Skip) .Take(parameter.Take) .ToList(); return Ok(new {items, total}); } 

在这种情况下,所有过滤数据的责任仅在于程序员。 他会记得有必要在Where添加条件吗?

您可以使用全局过滤器解决问题。 但是,为了限制访问,我们需要有关当前用户的信息,这意味着DbContext的构造必须很复杂才能初始化特定字段。

如果规则很多,那么DbContext的实现将不可避免地要学习“太多”,这将违反唯一责任原则

粉扑结构


发生数据访问和复制粘贴问题,因为在该示例中,我们忽略了将其划分为几层的问题,并且从控制器中绕过了业务逻辑层,我们立即到达了数据访问层。 这种方法甚至被称为“ 笨拙的笨拙的控制器” 。 在本文中,我不想涉及与存储库,服务和业务逻辑结构有关的问题。 全局过滤器可以很好地做到这一点,您只需要将它们应用于另一层的抽象即可。

添加抽象


.NET已经具有用于访问数据的IQueryableDbContext直接访问替换为对此类提供者的访问:

  public interface IQueryableProvider { IQueryable<T> Query<T>() where T: class; IQueryable Query(Type type); } 

为了访问数据,我们将创建以下过滤器:

  public interface IPermissionFilter<T> { IQueryable<T> GetPermitted(IQueryable<T> queryable); } 

我们以这种方式实现提供程序,使其搜索所有声明的过滤器并自动应用它们:

  public class QueryableProvider: IQueryableProvider { //       private static Type[] Filters = typeof(PermissionFilter<>) .Assembly .GetTypes() .Where(x => x.GetInterfaces().Any(y => y.IsGenericType && y.GetGenericTypeDefinition() == typeof(IPermissionFilter<>))) .ToArray(); private readonly DbContext _dbContext; private readonly IIdentity _identity; public QueryableProvider(DbContext dbContext, IIdentity identity) { _dbContext = dbContext; _identity = identity; } private static MethodInfo QueryMethod = typeof(QueryableProvider) .GetMethods() .First(x => x.Name == "Query" && x.IsGenericMethod); private IQueryable<T> Filter<T>(IQueryable<T> queryable) => Filters //     .Where(x => x.GetGenericArguments().First() == typeof(T)) //         Queryable<T> .Aggregate(queryable, (c, n) => ((dynamic)Activator.CreateInstance(n, _dbContext, _identity)).GetPermitted(queryable)); public IQueryable<T> Query<T>() where T : class => Filter(_dbContext.Set<T>()); //  EF Core  Set(Type type),    :( public IQueryable Query(Type type) => (IQueryable)QueryMethod .MakeGenericMethod(type) .Invoke(_dbContext, new object[]{}); } 

示例中用于获取和创建过滤器的代码不是最佳的。 而不是Activator.CreateInstance最好使用已编译的Expression Trees 。 一些IOC容器支持开放式仿制药的注册 。 我将把优化问题放在本文的范围之外。

我们实现过滤器


过滤器实现可能如下所示:

  public class EntityPermissionFilter: PermissionFilter<Entity> { public EntityPermissionFilter(DbContext dbContext, IIdentity identity) : base(dbContext, identity) { } public override IQueryable<Practice> GetPermitted( IQueryable<Practice> queryable) { return DbContext .Set<Practice>() .WhereIf(User.OrganizationType == OrganizationType.Client, x => x.Manager.OrganizationId == User.OrganizationId) .WhereIf(User.OrganizationType == OrganizationType.StaffingAgency, x => x.Partners .Select(y => y.OrganizationId) .Contains(User.OrganizationId)); } } 

我们更正控制器代码


  [HttpGet] public virtual IActionResult Get([FromQuery]T parameter) { var total = QueryableProvider .Query<TEntity>() .Where(/* some business rules */) .Count(); var items = QueryableProvider .Query<TEntity>() .Where(/* some business rules */) .ProjectTo<TDto>() .Skip(parameter.Skip) .Take(parameter.Take) .ToList(); return Ok(new {items, total}); } 

根本没有太多更改。 仍然禁止从控制器直接访问DbContext ,并且如果正确编写了过滤器,则可以认为数据访问已关闭。 过滤器非常小,因此用测试覆盖它们并不困难。 另外,这些相同的过滤器可用于编写授权代码,以防止未经授权访问“外部”数据。 我将这个问题留在下一篇文章中。

Source: https://habr.com/ru/post/zh-CN414897/


All Articles