Layer-Design:在哪里检查数据库读取/更新的权限?

时间:2015-11-29 17:30:52

标签: database scala model-view-controller web-applications playframework

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

因此,最终必须在当前请求的user-id和表示所请求日历的所有者的user-id之间进行比较。但我想知道在哪里这样做。我的想法:

  • 我可以在DAO级别上完成。但是,每个DAO方法都需要一个表示user-id的附加参数,这使得这些方法更加冗长并降低了可重用性。

    E.g。

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

    变为

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

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

  • 另一种选择可能是让DAO远离这个并在服务层或类似的东西中进行检查。这意味着在DAO返回请求的日历之后执行检查。这种方法听起来比另一种方法更灵活,但它也意味着如果用户无法访问所请求的日历,所请求的日历仍会消耗带宽和内存,因为它在任何一种情况下都是从数据库中提取的。

也许还有其他选项,我甚至没有考虑过。有没有最好的做法?

顺便说一下:我的网络应用程序中没有日历,这只是一个说明问题的例子。

6 个答案:

答案 0 :(得分:7)

我认为,关键是当你说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(可能是" admin"应用程序),那么您可以考虑将其子类化以提供访问控制您的常规应用程序",或者,可能只是创建一个额外的角色,允许用户访问有关所有权的所有日历,然后使用"超级用户"在您的管理应用程序中。

我不担心在检查访问权限之前必须加载对象的成本(即使加载对象非常昂贵,也应该是有人试图访问他不拥有的对象的罕见情况) 。我认为,针对服务层方法的更强有力的论据正是代码的可重用性和模块性:具有公共接口的DAO类的存在表明它至少可能被多个组件重用。考虑到这样的合同是不可执行的,特别是要求所有这些组件都执行自己的访问检查似乎很愚蠢:没有办法确定那些决定在几年后使用DAO类的人,将记住这个要求(或仔细阅读评论)。无论如何,如果要生成一个用于访问数据库的层,您也可以使它对某些内容有用。

答案 1 :(得分:4)

我使用第二种方法。我只会从架构的角度来看,我觉得这比通常很小的查询成本更重要。

一些原因包括:

  1. 将所有验证/验证放在一个地方更清晰。代码有一个结构。有些情况下,无法在DAO层中执行某些验证。然后它变成临时的,进入服务层的验证和DAO层中的内容是什么。

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

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

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

答案 2 :(得分:4)

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

  

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

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

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

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

  • 用户已通过身份验证,可确定每个经过身份验证的用户的日历访问权限(可以是"仅查看用户的日历"或者可以是"查看所有日历" ;)
  • 用户访问屏幕仅查看用户的日历 - 显示用户的日历。如果您认为管理员也应该使用同一个屏幕,那么您可以为管理员映射不同的服务,或者为管理员调用不同的DAO方法,或者为管理员传递不同的参数到DAO。
  • 用户访问查看所有日历的屏幕(嗯,此页面可能仅受管理员访问权限保护)。然后显示所有日历。如果你认为普通用户也应该使用同一个屏幕,那么你既可以为用户映射不同的服务,也可以为用户调用不同的DAO方法,或者为用户传递不同的DAO参数。

答案 3 :(得分:3)

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

  1. 您在应用程序服务器上节省资源
  2. 在数据库级别上过滤更快
  3. 尽快过滤,降低较高应用层的风险
  4.   

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

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

答案 4 :(得分:3)

在数据访问层上过滤是我的选择。 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);

答案 5 :(得分:3)

过滤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只需返回日历即可。