27.1 Overview
复杂的应用程序通常会发现需要定义访问权限,而不仅仅是在web请求或方法调用级别。相反,安全决策需要包括谁(身份验证)、在哪里(方法位置)和什么(某些域对象)。换句话说,授权决策还需要考虑方法调用的实际域对象实例主体。
假设你正在为宠物诊所设计一个应用程序。基于Spring的应用程序将有两个主要的用户群体:宠物诊所的工作人员,以及宠物诊所的客户。员工可以访问所有数据,而您的客户只能看到他们自己的客户记录。更有趣的是,您的客户可以允许其他用户查看他们的客户记录,例如他们的“幼犬学前班”导师或当地“小马俱乐部”的主席。使用Spring Security作为基础,您可以使用几种方法:
1、编写您的业务方法来加强安全性。您可以参考客户域对象实例中的集合来确定哪些用户可以访问。通过使用securitycontextholder . GetContext().getAuthentication(),您将能够访问身份验证对象。
2、写入一个访问决策投票者(AccessDecisionVoter ),以从存储在身份验证对象(Authentication)中的授予的权限中(GrantedAuthority[])强制实施安全性。这意味着您的身份验证管理器(AuthenticationManager )需要用代表主体有权访问的每个客户(Customer )域对象实例的自定义授权((GrantedAuthority[]))来填充身份验证(Authentication )。
3、编写一个访问决策投票器来加强安全性,并直接打开目标客户域对象。这意味着您的投票者需要访问一个允许它检索客户对象的DAO。然后,它将访问客户对象的已批准用户集合,并做出适当的决策。
这些方法中的每一种都是完全合法的。然而,第一种方法将您的授权检查与您的业务代码相结合。这方面的主要问题包括单元测试难度增加,以及在其他地方重用客户授权逻辑会更加困难。从身份验证对象(Authentication)中获取授予的权限[](GrantedAuthority[])也很好,但不会扩展到大量客户(customer)。如果一个用户能够访问5000个客户(在这种情况下不太可能,但是想象一下如果它是一个大型小马俱乐部的受欢迎的兽医!)构建身份验证对象所需的内存量和时间是不理想的。最后一种方法,直接从外部代码打开客户,可能是三种方法中最好的。它实现了关注点的分离,并且不会滥用内存或CPU周期,但是它仍然是低效的,因为访问决策表决器和最终的业务方法本身都将执行对负责检索客户对象的DAO的调用。每个方法调用两次访问显然是不可取的。此外,列出每种方法后,您需要从头开始编写自己的访问控制列表(ACL)持久性和业务逻辑。
幸运的是,还有另一种选择,我们将在下面讨论。
27.2 Key Concepts(关键概念)
Spring Security的ACL服务是在spring-security-acl-xxx.jar中提供的。
Spring Security的域对象实例安全功能集中在访问控制列表的概念上。系统中的每个域对象实例都有自己的ACL,该ACL记录了哪些人可以和哪些人不能使用该域对象的详细信息。考虑到这一点,Spring Security为您的应用程序提供了三个主要的ACL相关功能:
1、一种有效检索所有域对象的ACL条目(并修改这些ACL)的方法
2、在调用方法之前,确保给定主体被允许使用您的对象的一种方法
3、一种确保给定主体在方法被调用后被允许使用您的对象(或它们返回的东西)的方法
如第一个要点所示,Spring Security ACL模块的主要功能之一是提供一种检索ACL的高性能方法。这个ACL存储库功能非常重要,因为您系统中的每个域对象实例都可能有几个访问控制条目,并且每个ACL都可能从树状结构中的其他ACL继承而来(这是Spring Security现成支持的,并且非常常用)。Spring Security的ACL功能经过精心设计,提供了ACL的高性能检索、可插拔缓存、死锁最小化的数据库更新、独立于ORM框架(我们直接使用JDBC)、适当的封装和透明的数据库更新。
鉴于数据库是ACL模块操作的核心,让我们探索一下实现中默认使用的四个主表。在典型的Spring Security ACL部署中,下表按大小顺序显示,最后列出的行最多:
1、ACL_SID允许我们唯一地识别系统中的任何主体或机构(“SID”代表“安全身份”)。唯一的列是ID,SID的文本表示,指示文本表示是指主体名称还是授权的标志。因此,每个唯一的主体或授予的授权都有一行。当在接收许可的上下文中使用时,SID通常被称为“接收者”。
2、ACL _ CLASS允许我们唯一地识别系统中的任何域对象类。唯一的列是ID和Java类名。因此,对于我们希望存储ACL权限的每个唯一类,都有一行。
3、ACL_OBJECT_IDENTITY存储系统中每个唯一域对象实例的信息。列包括ID,ACL_CLASS表的外键、唯一标识符,这样我们就知道我们为哪个ACL_CLASS实例提供了信息父项,表示域对象实例所有者的ACL_SID表的外键,以及我们是否允许ACL条目从任何父ACL继承。我们为存储ACL权限的每个域对象实例都有一行。
4、最后,ACL_ENTRY存储分配给每个收件人的单独权限。列包括ACL_OBJECT_IDENTITY的外键,接收方(即ACL_SID的外键),不管我们是否进行审核,以及代表实际许可被授予或拒绝的整数位掩码。我们为每个接收到使用域对象的权限的收件人设置了一行。
如最后一段所述,ACL系统使用整数位屏蔽。不要担心,您不需要知道使用ACL系统时位移位的细微之处,但只要说我们有32位可以打开或关闭就足够了。每个位代表一个权限,默认情况下,权限为读取(位0)、写入(位1)、创建(位2)、删除(位3)和管理(位4)。如果您希望使用其他权限,那么很容易实现您自己的权限实例,并且ACL框架的其余部分将在不知道您的扩展的情况下运行。
重要的是要理解,您的系统中的域对象的数量与我们选择使用整数位掩码的事实毫无关系。虽然您有32位可用于权限,但您可能有数十亿个域对象实例(这意味着ACL_OBJECT_IDENTITY中有数十亿行,很可能还有ACL_ENTRY)。我们提出这一点是因为我们发现,有时人们错误地认为他们需要为每个潜在的领域对象分配一点,但事实并非如此。
现在,我们已经提供了ACL系统的基本概述,以及它在表结构中的样子,让我们来探索关键接口。主要接口是:
1、Acl:每个域对象都有一个并且只有一个Acl对象,它在内部保存了AccessControlEntry并知道Acl的所有者。Acl不直接引用域对象,而是引用ObjectIdentity。Acl存储在ACL_OBJECT_IDENTITY表中。
2、AccessControlEntry:一个Acl包含多个AccessControlEntry,在框架中通常缩写为ACEs。每个ACE指的是权限、样本号和Acl的特定元组。ACE也可以是授予或不授予的,并包含审计设置。ACE存储在ACL_ENTRY表中。
3、Permission:权限代表特定的不可变位掩码,并为位掩码和输出信息提供方便的功能。上面给出的基本权限(第0到第4位)包含在BasePermission类中。
4、Sid:ACL模块需要引用主体并授予授权。一个间接级别由Sid接口提供,Sid接口是“security identity”的缩写。常见的类包括PrincipalSid(代表Authentication 对象中的主体)和GrantedAuthoritySid。安全身份信息存储在ACL_SID表中。
5、ObjectIdentity:每个域对象在ACL模块内部由一个ObjectIdentity来表示。默认实现称为ObjectIdentityImpl。
6、AclService:检索适用于给定ObjectIdentity的Acl。在包含的实现(JdbcAclService)中,检索操作被委托给一个LookupStrategy。LookupStrategy提供了一种高度优化的策略来检索ACL信息,使用批处理检索(BasicLookupStrategy),并支持利用物化视图、分层查询和类似的以性能为中心的非ANSI SQL功能的自定义实现。
7、MutableAclService:允许为持久性呈现修改后的Acl。如果您不希望使用此界面,这并不是必须的。
请注意,我们现成的AclService和相关的数据库类都使用ANSI SQL。因此,这应该适用于所有主要的数据库。在撰写本报告时,该系统已经成功地通过了高超音速SQL、PostgreSQL、微软SQL Server和甲骨文的测试。
Spring Security附带的两个示例演示了ACL模块。第一个是联系人示例,另一个是文档管理系统示例。我们建议看一下这些例子。
27.3 Getting Started
要开始使用Spring Security的ACL功能,您需要将您的ACL信息存储在某个地方。这就需要使用Spring实例化数据源。然后,DataSource 被注入到一个JdbcMutableAclService和BasicLookupStrategy实例中。后者提供了高性能的ACL检索功能,而前者提供了变异功能。有关示例配置,请参考Spring Security附带的示例之一。您还需要用最后一节中列出的四个特定于ACL的表来填充数据库(有关适当的SQL语句,请参考ACL示例)。
一旦您创建了所需的模式并实例化了JdbcMutableAclService,接下来您需要确保您的域模型支持与Spring Security ACL包的互操作性。希望ObjectIdentityImpl将被证明是足够的,因为它提供了大量可以使用它的方法。大多数人将拥有包含public Serializable getId()方法的域对象。如果返回类型很长,或者与long(例如int)兼容,您会发现您不需要进一步考虑ObjectIdentity问题。ACL模块的许多部分依赖于长标识符。如果你不使用long(或int、byte等),很有可能你需要重新实现一些类。我们不打算在Spring Security的ACL模块中支持非长标识符,因为长标识符已经与所有数据库序列(最常见的标识符数据类型)兼容,并且具有足够的长度来适应所有常见的使用场景。
下面这段代码展示了如何创建一个Acl,或者修改一个现有的Acl:
在上面的例子中,我们正在检索与标识符号为44的“Foo”域对象相关联的ACL。然后我们添加一个ACE,这样一个名为“Samantha”的主体就可以“管理”这个对象。除了insertAce方法之外,代码片段相对来说是不言自明的。insertAce方法的第一个参数是确定新条目将被插入到Acl的什么位置。在上面的例子中,我们只是将新的ACE放在现有ACE的末尾。最后一个参数是一个布尔值,指示ACE是批准还是拒绝。大多数情况下,它将授予(真),但如果它拒绝(假),权限实际上被阻止。
作为您的DAO或存储库操作的一部分,Spring Security不提供任何特殊的集成来自动创建、更新或删除ACL。相反,您需要为您的单个域对象编写如上所示的代码。值得考虑的是,在您的服务层上使用AOP来自动将ACL信息与您的服务层操作集成在一起。过去我们发现这是一种非常有效的方法。一旦您使用上述技术在数据库中存储了一些ACL信息,下一步就是实际使用ACL信息作为授权决策逻辑的一部分。你在这里有很多选择。您可以编写自己的访问决策表决器(AccessDecisionVoter )或调用后提供程序(AfterInvocationProvider ),分别在方法调用之前或之后触发。这样的类将使用AclService来检索相关的ACL,然后调用ACl . IsGranted(Permission[]Permission,Sid[]Sid,boolean administrativeMode)来决定是授予还是拒绝权限。或者,您可以使用我们的AclEntryVoter、AclEntryAfterInvocationProvider或aclentryafterinvocationcollectionfiltering provider类。所有这些类都提供了一种基于声明的方法来在运行时评估ACL信息,使您无需编写任何代码。请参考示例应用程序,了解如何使用这些类。