层设计:在哪里检查数据库读取/更新的权限?

Posted

技术标签:

【中文标题】层设计:在哪里检查数据库读取/更新的权限?【英文标题】:Layer-Design: where to check permissions for database reads/updates? 【发布时间】:2016-03-03 07:54:27 【问题描述】:

在大多数情况下,您只希望用户能够访问由用户自己创建的数据库中的实体。例如,如果有一个由 User1 创建的日历,那么只有 User1 应该能够读取、更新或删除这个特定的日历及其在数据库中的内容。这通常与授权无关 - 在我的项目中,已经有一个基于角色的授权组件,它检查用户是否属于“日历编辑器”角色,但它不检查是否允许特定用户访问特定的日历。

因此,最终必须在当前请求的用户 ID 和代表所请求日历所有者的用户 ID 之间进行比较。但我想知道在哪里做这个。我的想法:

我可以在 DAO 级别上做到这一点。但是每个 DAO 方法都需要一个额外的参数来表示用户 ID,这使得这些方法更加冗长并降低了可重用性。

例如

def findCalById(id: Int): Future[Option[Calendar]]

变成

def findCalById(id: Int, ownerId: Int ): Future[Option[Calendar]]

一个优点是权限检查基本上是在查询级别完成的,这意味着如果用户无权访问日历,则不会从数据库返回日历。但话又说回来:如果在某些情况下没有返回日历,您如何区分不存在的日历和当前用户无法访问的现有日历?两种不同的场景,产生相同的结果。

另一种选择可能是将 DAO 排除在外,并在服务层或类似的东西中进行检查。这意味着检查是在 DAO 返回请求的日历之后执行的。这种方法听起来比另一种更灵活,但这也意味着,如果用户无法访问请求的日历,则请求的日历仍然会消耗带宽和内存,因为它是从数据库中获取的。

也许还有我什至没有考虑过的其他选项。有什么最佳做法吗?

顺便说一句:我的 Web 应用程序中没有日历,这只是说明问题的一个示例。

【问题讨论】:

【参考方案1】:

我认为,当您说 DAO 方法“降低可重用性”时,关键是要考虑您的确切含义。如果您强制执行用户访问权限的要求对您的 DAO 的所有应用程序都是通用的,那么在 DAO 级别执行此操作实际上增加可重用性而不是降低它:使用 DAO 的每个人都可以从这些中受益检查,而不必自己实施。

您可以将用户 id 设置为隐式参数,以使这些方法对上游用户更友好。您还可以让它返回一个 Try(或者,也许是一个 Either)来解决您对区分丢失和无法访问的对象情况的担忧:

case class UserId(id: Int)
def findCalById(id: Int)(implicit user: UserId): Future[Try[Option[Calendar]]] = ???

然后,调用者可以这样做:

implicit val currentUser = UserId(request.getUserId)
dao.findCalById(request.getCalendarId).map  
    case Failure(IllegalAccessException()) => "You are not allowed to use this calendar"
    case Return(None) => "Calendar not found"
    case Return(Some(cal)) => calendarToString(cal)

另一方面,如果有可能在没有用户上下文的情况下使用 DAO(可能是“管理员”应用程序),那么您可能会考虑将其子类化以提供对“常规应用程序”的访问控制,或者,也许只是简单地创建一个额外的角色,允许用户访问有关所有权的所有日历,然后在您的管理应用程序中使用该“超级用户”。

我不会担心在检查访问之前必须加载对象的成本(即使加载对象确实很昂贵,但应该很少发生有人试图访问他不拥有的对象) .我认为,反对服务层方法的一个更强有力的论据正是代码的可重用性和模块化:具有公共接口的 DAO 类的存在表明它至少可以被多个组件重用。要求所有此类组件实现自己的访问检查似乎很愚蠢,特别是考虑到此类合同将无法执行:无法确保几年后决定使用您的 DAO 类的某人,会记住这个要求(或注意阅读评论)。如果您无论如何都要生成一个用于访问数据库的层,那么您不妨让它对某些东西有用。

【讨论】:

【参考方案2】:

我使用第二种方法。我只会从架构的角度出发,我认为这比通常很小的查询成本更重要。

一些原因包括:

    将所有验证/验证放在一个地方会更简洁。代码有一个结构。会有一些验证无法在 DAO 层执行的情况。然后它变得特别,什么是验证进入服务层,什么在 DAO 层。

    方法 findCalById 应该只返回 Calendar 通过 id 使用一个 id。它更可重复使用。如果明天您需要一个管理员可以查看所有日历的功能,而不管所有者是谁。您最终将为此功能编写另一个查询。在服务层中添加此检查会更容易。

    假设有一天你有另一个数据存储返回记录,那么你最终会在多个地方进行验证。如果有服务层进行验证,则不会发生这种情况。服务层不会改变,因为它不会关心记录来自哪里。

    让新同事入职变得更加容易。假设一个专门研究数据库领域的新人开始与您合作。如果他只关心数据库应该返回的记录而忘记应用程序如何使用这些数据,他的工作效率会更高。 (关注点分离也适用于现实生活:))。

【讨论】:

【参考方案3】:

您在这里提出了一个非常有趣的问题。正如其他人已经强调的那样,正确的方法取决于您问题的含义。

如何区分不存在的日历和 当前用户无法访问的现有日历?

当您开始考虑在 DAO 与服务中实现过滤逻辑时,您实际上开始解决多租户问题,这可能是already solved。这听起来像是多租户,因为您考虑如何隔离使用同一应用程序的不同用户之间的数据。由于您想要共享数据库,因此问题已缩小 - 这是给定的(无法更改)。您还提到数据并不总是孤立的(管理员可以看到其他人在孤立的情况下看到的内容)。如果是这样,那么实现位置实际上是不相关,并且对于每个用例,您可以有不同的选择——无论哪个对特定的用例有意义。如果您没有混合用例,但您确实有(管理员与普通用户),这很重要。因此,您的多租户并不复杂 - 它只是基于用例。

在这次谈话中我看到一件尴尬的事情是 - 你认为你的数据库与你的应用程序是分开的,这实际上违背了数据库的目的。您的数据库是应用程序的数据库。我还看到了将数据访问逻辑与其他层分开考虑的迹象,这实际上使您的数据访问不同的应用程序,但事实并非如此 - 所有层一起 构成了您的应用程序。正是这种对应用程序的整体视图使得实施地点变得无关紧要[在这种非常具体的情况下]。

以下是我对您的应用程序中可能存在的几个用例的看法:

用户经过身份验证,这决定了每个经过身份验证的用户对日历的访问权限(可以是“仅查看用户的日历”,也可以是“查看所有日历”)。 用户访问屏幕以仅查看用户的日历 - 显示用户的日历。如果您认为 Admin 也应该使用相同的屏幕,那么您可以为 admin 映射不同的服务,或者为 admin 调用不同的 DAO 方法,或者为 admin 传递不同的参数给 DAO。 用户访问屏幕以查看所有日历(好吧,此页面可能仅受管理员访问保护)。然后显示所有日历。如果您认为普通用户也应该使用同一个屏幕,那么您可以为用户映射不同的服务,或者为用户调用不同的 DAO 方法,或者为用户传递不同的参数给 DAO。

【讨论】:

【参考方案4】:

DAO 级别的过滤结果是一种不错的方法,原因如下:

    您在应用服务器上节省资源 数据库级别的过滤速度更快 尽快过滤以降低更高应用层的错误风险

如何区分不存在的日历和现有的日历 当前用户无法访问的日历?

出于安全原因,您根本不应该显示无法访问的对象,但有时可用性更重要。区分可能性应取决于您的应用特定。

【讨论】:

【参考方案5】:

对数据访问层进行过滤是我的选择。 Normaly 我在一个名为 DAL 的单独类库中分离我对数据库的访问。在该库中,我使用返回数据的方法定义了一个接口。当您创建该接口的实例时,将有一个带有用户参数的构造函数。因此,界面将为您过滤数据,而无需在每个方法上传递用户信息。

public class DatabaseInterface 
    private UserIdentity UserInfo;
    private Database Data;

    public DatabaseInterface(UserIdentity user) 
        UserInfo = user;
        Data = new Database();
    

    public List<cal> findCalById(int id) 
        return Data.cal.Where(x => x.user == this.UserInfo && x.id == id).ToList();
    

接口的使用

var dal = new DatabaseInterface(user);
var myData = dal.findCalById(1);

【讨论】:

更多信息在这里:***.com/questions/59942/…【参考方案6】:

我更喜欢在 DAO 层过滤结果。

作为减少参数列表的一种方式,由于日历是从所有者的角度检索的,因此无需传入日历ID。 而不是这样做: def findCalById(id: Int, ownerId: Int ): Future[Option[Calendar]],我会做的:def findCal(ownerId: Int): Future[Option[Calendar]]

关于:

如何区分不存在的日历和当前用户无法访问的现有日历?

使用def findCal(ownerId: Int): Future[Option[Calendar]] 方法,您甚至不需要区分这两种情况。因为从用户/所有者的角度来看,DAO 只需要返回日历(如果存在)。

【讨论】:

以上是关于层设计:在哪里检查数据库读取/更新的权限?的主要内容,如果未能解决你的问题,请参考以下文章

检查权限的最佳位置在哪里?

如何检查用户是不是能够在 marklogic 数据库中更新或插入文档?

关于权限的动态表创建:设计问题

尝试从两个数据库中读取以检查帐户是不是存在

N-Tier - 插入与更新的责任位置

mysql中未读消息的数据库设计?