图层设计:在哪里检查数据库 reads/updates 的权限?
Layer-Design: where to check permissions for database reads/updates?
在大多数情况下,您希望用户只能访问用户自己创建的数据库中的实体。例如,如果 User1 创建了一个日历,那么只有 User1 应该能够读取、更新或删除这个特定的日历及其数据库中的内容。这与一般授权无关——在我的项目中,已经有一个基于角色的授权组件,它检查用户是否属于 "calendar-editor" 角色,但它不检查是否允许特定用户访问具体日历。
因此最终必须比较当前请求的用户 ID 和代表所请求日历所有者的用户 ID。但我想知道在哪里做这个。我的想法:
我可以在 DAO 级别上完成。但是每个 DAO 方法都需要一个额外的参数来表示用户 ID,这使得这些方法更加冗长并降低了可重用性。
例如
def findCalById(id: Int): Future[Option[Calendar]]
变成
def findCalById(id: Int, ownerId: Int ): Future[Option[Calendar]]
一个优点是权限检查基本上是在查询级别完成的,这意味着如果用户无权访问日历,则不会从数据库返回任何日历。但话又说回来:如果在某些情况下没有返回日历,你如何区分不存在的日历和当前用户无法访问的现有日历?两种不同的场景,产生相同的结果。
另一种选择可能是将 DAO 排除在外,并在服务层或类似的东西中进行检查。这意味着检查是在 DAO 返回请求的日历之后执行的。这种方法听起来比另一种方法更灵活,但这也意味着如果用户无法访问所请求的日历,所请求的日历仍然会消耗带宽和内存,因为在任何一种情况下它都是从数据库中获取的。
也许还有其他我什至没有考虑过的选项。有什么最佳做法吗?
顺便说一句:我的 Web 应用程序中没有日历,这只是一个说明问题的示例。
出于以下几个原因,在 DAO 级别过滤结果是一种很好的方法:
- 您节省了应用程序服务器上的资源
- 数据库级别的过滤速度更快
- 尽快过滤降低更高应用层的错误风险
how to distinguish between a calendar that does not exist and an existing
calendar that cannot be accessed by the current user?
出于安全原因,您根本不应显示无法访问的对象,但有时可用性更为重要。区分可能性应该取决于您的应用程序具体。
在数据访问层上过滤将是我的选择。通常我将对数据库的访问分开在一个名为 DAL 的单独 class 库中。在该库中,我使用 return 数据的方法定义了一个接口。当您创建该接口的实例时,将有一个具有用户参数的构造函数。因此界面将为您过滤数据,而无需在每个方法上传递用户信息。
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);
我更喜欢在 DAO 层中过滤结果。
作为减少参数列表的一种方式,由于日历是从所有者的角度检索的,因此无需传入日历 ID。
而不是做:
def findCalById(id: Int, ownerId: Int ): Future[Option[Calendar]]
,我会做:def findCal(ownerId: Int): Future[Option[Calendar]]
。
关于:
How to distinguish between a calendar that does not exist and an existing calendar that cannot be accessed by the current user?
使用def findCal(ownerId: Int): Future[Option[Calendar]]
方法,您甚至不需要区分这两种情况。因为从 user/owner 的角度来看,DAO 只需要 return 日历(如果存在的话)。
我用的是第二种方法。我只会从架构的角度出发,我认为这比通常很小的查询成本更重要。
一些原因包括:
将所有 validations/verifications 放在一个地方会更干净。代码有一个结构。 DAO 层会出现某些验证无法执行的情况。然后它变成了临时的,什么验证进入服务层,什么进入 DAO 层。
方法 findCalById
应该只 return Calendar
通过 id 使用 id。它更可重复使用。如果明天您需要管理员可以查看所有日历(无论所有者是谁)的功能怎么办?您最终将为此功能编写另一个查询。在服务层中添加此检查会更容易。
假设有一天您有另一个数据存储 return 记录,那么您最终会在多个地方进行验证。如果有一个服务层来进行验证,就不会发生这种情况。服务层不会改变,因为它不会关心记录来自哪里。
新同事入职变得更加容易。假设一个专门研究数据库领域的新人开始和你一起工作。如果他只关心数据库应该 return 忘记应用程序如何使用这些数据的记录,他会更有效率。 (关注点分离也适用于现实生活:))。
我认为,当你说 DAO 方法 "decreases reusability" 时,关键是要思考你到底是什么意思。如果您强制执行用户访问权限的要求对于您的 DAO 的所有应用程序都是通用的,那么在 DAO 级别执行此操作实际上 增加 可重用性而不是降低它:每个使用 DAO 的人都能够从这些检查中受益,而不必自己实施它们。
您可以将用户标识设为隐式参数,使这些方法对上游用户更加友好。您还可以 return 尝试(或者可能是 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" 应用程序),那么您可以考虑将其子class为您的 "regular applications" 提供访问控制,或者,也许只是创建一个额外的角色,允许用户访问与所有权有关的所有日历,然后在您的管理应用程序中使用该 "superuser"。
我不会担心在检查访问之前必须加载对象的成本(即使加载对象真的很昂贵,也应该很少有人尝试访问他没有访问的对象自己的)。我认为,反对服务层方法的一个更有力的论据恰恰是代码的可重用性和模块化:DAO class 和 public 接口的存在表明它可以,至少有可能,被重用由不止一个组成部分。要求所有这些组件实现它们自己的访问检查似乎很愚蠢,特别是考虑到这样的合同将无法执行:没有办法确保决定使用你的 DAO class 的人多年后,会记住这个要求(或注意阅读评论)。如果您正在生成一个用于访问数据库的层,您不妨将其用于某些用途。
你在这里提出了一个非常有趣的问题。正如其他人已经强调的那样,正确的方法取决于您问题的含义。
how to distinguish between a calendar that does not exist and an
existing calendar that cannot be accessed by the current user?
当您开始考虑在 DAO 和服务中实现过滤逻辑时,您实际上开始解决多租户问题,这可能是 already solved。这听起来像是多租户,因为您考虑如何隔离使用同一应用程序的不同用户之间的数据。由于您想拥有共享数据库,因此问题范围缩小了-这是给定的(无法更改)。您还提到数据并不总是孤立的(管理员可以看到其他人在孤立的情况下看到的内容)。如果是这样,那么实施地点实际上是 无关 ,并且对于每个用例,您可以有不同的选择 - 以对特定用例有意义的为准。如果您没有混合用例,但您确实有(管理员与普通用户),那将很重要。因此,您的多租户并不那么复杂 - 它只是基于用例的。
在这次谈话中我看到的一件尴尬的事情是——你认为你的数据库与你的应用程序是分开的,这实际上违背了数据库的目的。您的数据库是应用程序的数据库。我还看到了将您的数据访问逻辑与其他层分开考虑的迹象,这实际上使您的数据访问不同的应用程序,但事实并非如此 - 所有层 一起 构成您的应用程序。 [在这种非常具体的情况下],正是这种应用程序的整体视图使得实施位置变得无关紧要。
以下是我对您的应用程序中可能存在的几个用例的看法:
- 用户已通过身份验证,这决定了每个经过身份验证的用户对日历的访问权限(可以是 "View only user's calendars",也可以是 "View all calendars")。
- 用户访问屏幕以仅查看用户的日历 - 显示用户的日历。如果您认为 Admin 也应该使用相同的屏幕,那么您可以为 admin 映射不同的服务,或者为 admin 调用不同的 DAO 方法,或者为 admin 向 DAO 传递不同的参数。
- 用户访问屏幕以查看所有日历(好吧,此页面可能仅受管理员访问保护)。然后显示所有日历。如果您认为普通用户也应该使用相同的屏幕,那么您可以为用户映射不同的服务,或者为用户调用不同的 DAO 方法,或者为用户向 DAO 传递不同的参数。
在大多数情况下,您希望用户只能访问用户自己创建的数据库中的实体。例如,如果 User1 创建了一个日历,那么只有 User1 应该能够读取、更新或删除这个特定的日历及其数据库中的内容。这与一般授权无关——在我的项目中,已经有一个基于角色的授权组件,它检查用户是否属于 "calendar-editor" 角色,但它不检查是否允许特定用户访问具体日历。
因此最终必须比较当前请求的用户 ID 和代表所请求日历所有者的用户 ID。但我想知道在哪里做这个。我的想法:
我可以在 DAO 级别上完成。但是每个 DAO 方法都需要一个额外的参数来表示用户 ID,这使得这些方法更加冗长并降低了可重用性。
例如
def findCalById(id: Int): Future[Option[Calendar]]
变成
def findCalById(id: Int, ownerId: Int ): Future[Option[Calendar]]
一个优点是权限检查基本上是在查询级别完成的,这意味着如果用户无权访问日历,则不会从数据库返回任何日历。但话又说回来:如果在某些情况下没有返回日历,你如何区分不存在的日历和当前用户无法访问的现有日历?两种不同的场景,产生相同的结果。
另一种选择可能是将 DAO 排除在外,并在服务层或类似的东西中进行检查。这意味着检查是在 DAO 返回请求的日历之后执行的。这种方法听起来比另一种方法更灵活,但这也意味着如果用户无法访问所请求的日历,所请求的日历仍然会消耗带宽和内存,因为在任何一种情况下它都是从数据库中获取的。
也许还有其他我什至没有考虑过的选项。有什么最佳做法吗?
顺便说一句:我的 Web 应用程序中没有日历,这只是一个说明问题的示例。
出于以下几个原因,在 DAO 级别过滤结果是一种很好的方法:
- 您节省了应用程序服务器上的资源
- 数据库级别的过滤速度更快
- 尽快过滤降低更高应用层的错误风险
how to distinguish between a calendar that does not exist and an existing calendar that cannot be accessed by the current user?
出于安全原因,您根本不应显示无法访问的对象,但有时可用性更为重要。区分可能性应该取决于您的应用程序具体。
在数据访问层上过滤将是我的选择。通常我将对数据库的访问分开在一个名为 DAL 的单独 class 库中。在该库中,我使用 return 数据的方法定义了一个接口。当您创建该接口的实例时,将有一个具有用户参数的构造函数。因此界面将为您过滤数据,而无需在每个方法上传递用户信息。
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);
我更喜欢在 DAO 层中过滤结果。
作为减少参数列表的一种方式,由于日历是从所有者的角度检索的,因此无需传入日历 ID。
而不是做:
def findCalById(id: Int, ownerId: Int ): Future[Option[Calendar]]
,我会做:def findCal(ownerId: Int): Future[Option[Calendar]]
。
关于:
How to distinguish between a calendar that does not exist and an existing calendar that cannot be accessed by the current user?
使用def findCal(ownerId: Int): Future[Option[Calendar]]
方法,您甚至不需要区分这两种情况。因为从 user/owner 的角度来看,DAO 只需要 return 日历(如果存在的话)。
我用的是第二种方法。我只会从架构的角度出发,我认为这比通常很小的查询成本更重要。
一些原因包括:
将所有 validations/verifications 放在一个地方会更干净。代码有一个结构。 DAO 层会出现某些验证无法执行的情况。然后它变成了临时的,什么验证进入服务层,什么进入 DAO 层。
方法
findCalById
应该只 returnCalendar
通过 id 使用 id。它更可重复使用。如果明天您需要管理员可以查看所有日历(无论所有者是谁)的功能怎么办?您最终将为此功能编写另一个查询。在服务层中添加此检查会更容易。假设有一天您有另一个数据存储 return 记录,那么您最终会在多个地方进行验证。如果有一个服务层来进行验证,就不会发生这种情况。服务层不会改变,因为它不会关心记录来自哪里。
新同事入职变得更加容易。假设一个专门研究数据库领域的新人开始和你一起工作。如果他只关心数据库应该 return 忘记应用程序如何使用这些数据的记录,他会更有效率。 (关注点分离也适用于现实生活:))。
我认为,当你说 DAO 方法 "decreases reusability" 时,关键是要思考你到底是什么意思。如果您强制执行用户访问权限的要求对于您的 DAO 的所有应用程序都是通用的,那么在 DAO 级别执行此操作实际上 增加 可重用性而不是降低它:每个使用 DAO 的人都能够从这些检查中受益,而不必自己实施它们。
您可以将用户标识设为隐式参数,使这些方法对上游用户更加友好。您还可以 return 尝试(或者可能是 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" 应用程序),那么您可以考虑将其子class为您的 "regular applications" 提供访问控制,或者,也许只是创建一个额外的角色,允许用户访问与所有权有关的所有日历,然后在您的管理应用程序中使用该 "superuser"。
我不会担心在检查访问之前必须加载对象的成本(即使加载对象真的很昂贵,也应该很少有人尝试访问他没有访问的对象自己的)。我认为,反对服务层方法的一个更有力的论据恰恰是代码的可重用性和模块化:DAO class 和 public 接口的存在表明它可以,至少有可能,被重用由不止一个组成部分。要求所有这些组件实现它们自己的访问检查似乎很愚蠢,特别是考虑到这样的合同将无法执行:没有办法确保决定使用你的 DAO class 的人多年后,会记住这个要求(或注意阅读评论)。如果您正在生成一个用于访问数据库的层,您不妨将其用于某些用途。
你在这里提出了一个非常有趣的问题。正如其他人已经强调的那样,正确的方法取决于您问题的含义。
how to distinguish between a calendar that does not exist and an existing calendar that cannot be accessed by the current user?
当您开始考虑在 DAO 和服务中实现过滤逻辑时,您实际上开始解决多租户问题,这可能是 already solved。这听起来像是多租户,因为您考虑如何隔离使用同一应用程序的不同用户之间的数据。由于您想拥有共享数据库,因此问题范围缩小了-这是给定的(无法更改)。您还提到数据并不总是孤立的(管理员可以看到其他人在孤立的情况下看到的内容)。如果是这样,那么实施地点实际上是 无关 ,并且对于每个用例,您可以有不同的选择 - 以对特定用例有意义的为准。如果您没有混合用例,但您确实有(管理员与普通用户),那将很重要。因此,您的多租户并不那么复杂 - 它只是基于用例的。
在这次谈话中我看到的一件尴尬的事情是——你认为你的数据库与你的应用程序是分开的,这实际上违背了数据库的目的。您的数据库是应用程序的数据库。我还看到了将您的数据访问逻辑与其他层分开考虑的迹象,这实际上使您的数据访问不同的应用程序,但事实并非如此 - 所有层 一起 构成您的应用程序。 [在这种非常具体的情况下],正是这种应用程序的整体视图使得实施位置变得无关紧要。
以下是我对您的应用程序中可能存在的几个用例的看法:
- 用户已通过身份验证,这决定了每个经过身份验证的用户对日历的访问权限(可以是 "View only user's calendars",也可以是 "View all calendars")。
- 用户访问屏幕以仅查看用户的日历 - 显示用户的日历。如果您认为 Admin 也应该使用相同的屏幕,那么您可以为 admin 映射不同的服务,或者为 admin 调用不同的 DAO 方法,或者为 admin 向 DAO 传递不同的参数。
- 用户访问屏幕以查看所有日历(好吧,此页面可能仅受管理员访问保护)。然后显示所有日历。如果您认为普通用户也应该使用相同的屏幕,那么您可以为用户映射不同的服务,或者为用户调用不同的 DAO 方法,或者为用户向 DAO 传递不同的参数。