访问控制列表(ACL)

在本章中,我们将会介绍访问控制列表这个复杂话题,它能够提供域对象实例层次授权的丰富模型。Spring Security提供了强大的访问控制列表,但是复杂且缺少文档,它能够很好的满足小到中型规模的实现。
在本章的内容中,我们将会:

理解访问控制列表的概念模型;

了解Spring Security ACL模型中的关于访问控制列表的术语和应用;

构建和了解支持Spring ACL的数据库模式;

配置JBCP Pets通过注解和Spring bean来使用ACL保护业务方法;

进行高级配置,包括自定义许可、使用ACL的JSP标签检查和方法安全,易变的ACL以及缓存;

了解架构要考虑的因素以及规划ACL部署的场景。

译者注:在本章中术语permission翻译为许可权限;entry翻译为条目;access control entry即为访问控制条目。

使用访问控制列表保护业务对象

最后一个非web层安全的问题是业务对象层次的安全,它在业务层或业务层以下。这个层次的安全实现使用了一项名为访问控制列表(access control list,或ACL)的技术。ACL中的对象可以进行权限判断——ACL允许基于唯一的组、业务对象以及逻辑操作进行特定的许可。

例如,在JBCP Pets中的一个ACL声明可能会是这样的:用户只能对自己的简介进行写操作。可能像下面展现的这样:

用户名对象许可权限
amyProfile123read,write
ROLE_USERProfile123read
ANONYMOUSAny Profilenone

你会发现这个ACL对人工来说很易读——Amy对自己的简介(Profile123)能够进行读写;其它的注册用户能够读Amy的简介,而匿名用户不能。简单来说,这种类型的规则矩阵就是ACL所试图做的,即将代码、访问检查、安全系统和业务数据的元数据进行集成。大多数真正使用ACL的系统会有很复杂的ACL列表以及整个系统中百万级的条目。尽管这听起来有令人感到害怕的复杂性,但是预先适当的思考和使用良好的安全库能够使得ACL管理相当可行。

如果你使用Microsoft Windows或者基于Unix/Linux的电脑,那你每天都在体验ACL。大多数现代电脑的操作系统都使用ACL指令作为文件存储系统的一部分,这允许基于用户或组、文件或目录以及许可权限的组合进行许可授权。在Microsoft Windows中,你能通过右键点击一个文件并查看其安全属性的方法(属性|安全标签)看到ACL功能:


你可以看见各种的组或用户以及权限,从而能够以可视化和很直观的方式输入ACL。

Spring Security中的访问控制列表

在安全系统中,Spring Security支持ACL驱动的授权检查用户访问某个域对象。与OS文件系统的例子很相似,可以使用Spring Security ACL组件构建业务对象以及组或安全实体的逻辑树结构。基于请求者和被请求对象得到的许可授权(继承或明确声明)被用来确定是否允许访问。

对于接触Spring Security ACL功能的用户来说,很可能被它的复杂性和缺乏文档、例子所吓倒。再加上建立ACL的基础设施很复杂,需要很多的相互依赖以及与Spring Security其它地方不同的基于bean的配置机制(我们等会建立初始配置的时候,你会看到)。

Spring Security的ACL模型比较基础,但是试图构建扩展功能的用户可能会发现一些令人沮丧的限制并且Spring Security早期引入的一些设计决策已经不合时宜。不要让这些限制把你弄得灰心。ACL模型是一个往你应用中嵌入丰富访问控制的强大方式,并且会进行仔细的审查以保护用户的行为和数据。

在我们开始配置Spring Security ACL之前,我们需要了解一些关键的术语和概念。

在Spring ACL系统中,主要的安全角色标识是security identity或SID。SID是一个逻辑组成,能够被用来抽象一个单独的安全实体或组(GrantedAuthority)。ACL数据模型定义的SID被用做确定安全实体访问级别的基础,而这个访问规则可以是明确指明的也可以是继承下来的。

如果SID被用作确定ACL系统的角色,那对应的另一半安全相关的就是安全对象本身了。单个的安全对象标识被称为(毫无意外的)对象标识(object identity)。默认的Spring ACL实现中,对象标识需要ACL规则定义在对象实例层级,这就意味着,如果需要系统中的每一个对象都能有单独的访问规则。

单个的访问规则就是所谓的访问控制条目(access control entries或ACEs)。一个ACE包含以下的元素:

规则应用的角色SID;

规则应用的对象标识;

应用于给定SID和对象标识的许可权限;

给定的许可权限对于特定的SID和对象标识应该允许还是拒绝(译者注:个人理解,此处指的是给定一个权限能够判断出是否允许访问)。

Spring ACL系统作为一个整体,其目的是评估每一个方法调用并确定方法中的对象针对每一个ACE是否可用。适当的ACE在运行时进行评估,这要基于调用者和使用的对象。

【Spring Security ACL在实现上是灵活的。尽管本章的内容大多数都是Spring Security模块内置的功能,但是要记住的是很多的规则是默认实现,在很多情况下能够基于复杂的需求进行重写。】

Spring Security使用一些有用的值对象来描述相关联的每一个概念实体。它们列到了以下的表格中:

ACL概念对象Java对象
SIDo.s.s.acls.model.Sid
对象标识(Object Identity)o.s.s.acls.model.ObjectIdentity
ACLo.s.s.acls.model.Acl
ACEo.s.s.acls.model.AccessControlEntry

让我们通过JBCP Pets store应用的一个例子来了解使用Spring Security ACL组件的过程。

支持Spring Security ACL的基本配置

尽管我们前面提到过在SpringSecurity中配置支持ACL需要基于bean的配置(它的确如此),但是你可以使用ACL却保持较为简单的security XML命名空间配置(译者注:即ACL需要基于bean的配置,但是原有的security命名空间配置可以保留)。如果你要运行示例代码,你需要切换web.xml到security命名空间配置,使用dogstore-base.xml和dogstore-security.xml。

定义一个简单的目标场景

我们一个简单的目标场景是不允许没有ROLE_ADMIN GrantedAuthority权限的用户访问主页上的第一个分类。第一个分类被称为“Pet Apparel”——这就是我们要用ACL安全保护的分类。

尽管有多种方式建立ACL检查,但是我们喜欢在第五章用到过的基于注解实现方法安全的方式。它很好的把使用ACL从实际接口声明中抽取出来,并允许你以后将角色声明替换为(如果愿意)除了ACL的其它方式。

我们将要在IProductService.getItemsByCategory方法上添加一个注解,它需要触发这个方法的任何人有适当的角色来查看这个分类:

  1. @Secured("VOTE_CATEGORY_READ")
  2. public Collection<Item> getItemsByCategory(Category cat);

这个方法被JBCP Pets站点的分类查看页面调用,而这个页面可以通过点击主页上的分类进入:


让我们看是进行配置的变化!

添加ACL表到HSQL数据库中

我们需要做的第一个事情是添加支持持久化ACL条目的表到我们的内存HSQL数据库中。要做到这一点,我们添加一些新的SQL DDL文件到我们的嵌入式数据库声明中,在dogstore-security.xml里:

  1. <jdbc:embedded-database id="dataSource" type="HSQL">
  2.   <jdbc:script location="classpath:security-schema.sql"/>
  3.   <jdbc:script location="classpath:test-data.sql"/>
  4.   <jdbc:script location="classpath:remember-me-schema.sql"/>
  5.   <jdbc:script location="classpath:test-users-groups-data.sql"/>
  6.   <jdbc:script location="classpath:acl-schema.sql"/>
  7. </jdbc:embedded-database>

acl-schema.sql文件会放在WEB-INF/classes文件夹下(或在应用类路径下的其它位置),并包含以下内容:

  1. create table acl_sid (
  2.   id bigint generated by default as identity(start with 100) not null
  3. primary key,
  4.   principal boolean not null,
  5.   sid varchar_ignorecase(100) not null,
  6.   constraint uk_acl_sid unique(sid,principal) );
  7. create table acl_class (
  8.   id bigint generated by default as identity(start with 100) not null
  9. primary key,
  10.   class varchar_ignorecase(500) not null,
  11.   constraint uk_acl_class unique(class) );
  12. create table acl_object_identity (
  13.   id bigint generated by default as identity(start with 100) not null
  14. primary key,
  15.   object_id_class bigint not null,
  16.   object_id_identity bigint not null,
  17.   parent_object bigint,
  18.   owner_sid bigint not null,
  19.   entries_inheriting boolean not null,
  20.   constraint uk_acl_objid unique(object_id_class,object_id_identity),
  21.   constraint fk_acl_obj_parent foreign key(parent_object)references
  22. acl_object_identity(id),
  23.   constraint fk_acl_obj_class foreign key(object_id_class)references
  24. acl_class(id),
  25.   constraint fk_acl_obj_owner foreign key(owner_sid)references acl_
  26. sid(id) );
  27. create table acl_entry (
  28.   id bigint generated by default as identity(start with 100) not null
  29. primary key,
  30.   acl_object_identity bigint not null,
  31.   ace_order int not null,
  32.   sid bigint not null,
  33.   mask integer not null,
  34.   granting boolean not null,
  35.   audit_success boolean not null,
  36.   audit_failure boolean not null,
  37.   constraint uk_acl_entry unique(acl_object_identity,ace_order),
  38.   constraint fk_acl_entry_obj_id foreign key(acl_object_identity)
  39.       references acl_object_identity(id),
  40.   constraint fk_acl_entry_sid foreign key(sid) references acl_sid(id)
  41. );

这将会生成如下的数据库模式:

你能够看到SID、对象标识以及ACE的概念如何匹配到数据库模式中。概念上讲,这很方便,因为我们能够匹配ACL系统的模型并直接将其关联到数据库中。

如果你将这与Spring Security文档中提供的HSQL数据库模式进行对比,你会发现我们进行了一些改变,这可能会使用户走弯路。如下:

修改ACL_CLASS.CLASS列从100个字符到500个字符。一些满足要求的长类名可能超过100个字符;

外键的名字更有意义,这样失败能够更容易的进行诊断。

【如果你使用其它的数据库,如oracle,你需要将这个DDL转换成你特定数据库的数据类型。】

一旦我们配置完ACL系统的其它部分,我们将会回到数据库这边建立一些基本的ACE以证明ACL的基本功能。

配置访问决策管理器

我们需要配置<global-method-security>以启用注解(我们将在这里注明基于ACL的权限)并引用一个自定义的访问决策管理器(access decision manager)。

【用于ACL检查的访问决策管理器必须与用于检查web URL授权规则的进行区分。这个需求源于传递给决策管理器的所有认证检查必须被它所有配置的voter所支持。不幸的是,web请求认证与方法请求认证的处理方法不一样,所以需要一个单独配置的访问决策器。当我们在第五章:精确的访问控制第一次接触方法安全时,我们不需要明确进行这个配置变化,因为security命名空间解析所实例化的访问控制管理器足够满足我们的要求。】

配置访问决策管理器的引用被设置在dogstore-security.xml文件中,如下:

  1. <global-method-security secured-annotations="enabled"
  2. access-decision-manager-ref="aclDecisionManager"/>

这是对访问控制管理器的一个bean引用,我们定义在dogstore-base.xml文件中:

  1. <bean class="org.springframework.security.access.vote.AffirmativeBased"
  2.  id="aclDecisionManager">
  3.   <property name="decisionVoters">
  4.     <list>
  5.       <ref bean="categoryReadVoter"/>
  6.     </list>
  7.   </property>
  8. </bean>

我们将会在练习的下一步中从投票器开始处理这个引用链。要记住的是这个AccessDecisionManager像其它的一样,自然也像web认证决策管理器那样,依赖于投票器做授权决策。

配置ACL的支持bean

即使是进行一个相对很简单的ACL配置,就像我们的场景那样,也有许多需要的依赖要建立。正如我们前面提到的,Spring Security ACL模块自带了很多的组件,你可以对它们进行组装以提供很好的ACL功能。注意的是,下图所引用的所有组件只是框架的一部分!

ACL子系统与Spring Security框架其它大部分功能有一个明显的不同就是大多数的配置需要构造方法注入(constructor injection),而不是属性注入(property injection)。正如我们在第六章:高级配置和扩展中明确bean配置所讨论的那样,一部分Spring Security在这方面并不一致,强制要求使用构造器以确保需要的属性被设置——这在配置大多数ACL相关组件时是需要注意的。

让我们从配置categoryReadVoter开始。这是AccessDecisionVoter的实现,它会访问ACL存储(在数据库中)并进行运行时的认证做出访问决策。

  1. <bean class="org.springframework.security.acls.AclEntryVoter"
  2. id="categoryReadVoter">
  3.   <constructor-arg ref="aclService"/>
  4.   <constructor-arg value="VOTE_CATEGORY_READ"/>
  5.   <constructor-arg>
  6.     <array>
  7.       <util:constant static-field="org.springframework.security.acls.
  8. domain.BasePermission.READ"/>
  9.     </array>
  10.   </constructor-arg>
  11.   <property name="processDomainObjectClass"
  12.  value="com.packtpub.springsecurity.data.Category"/>
  13. </bean>

不是明显bean引用的属性需要进行一些说明。第二个构造参数,我们给它一个值为VOTE_CATEGORY_READ,用来表明这个投票器能够投票的安全属性。你可能会记得这与我们要进行ACL保护的方法上的@Secured注解匹配。

第三个构造参数以及属性联合起来声明了能够对请求做出一个成功投票的ACL属性。在本例中,声明的投票器用来对包含VOTE_CATEGORY_READ的方法访问进行授权,请求者必须有ACL条目来表明他允许对com.packtpub.springsecurity.data.Category域对象进行READ访问。

我们可以看到,ACL投票器的声明是一个抽象层,并且位于代码资源声明的授权要求和域对象对ACL SID分配许可权限之间。这层抽象使得对安全资源分配有意义的属性方面有了很大的灵活性。

引用aclService的bean处理的是o.s.s.acls.model.AclService的一个实现,它负责(通过代理)将ACL保护的对象转换成期望的ACE。

  1. <bean class="org.springframework.security.acls.jdbc.JdbcAclService"  id="aclService">
  2.   <constructor-arg ref="dataSource"/>
  3.   <constructor-arg ref="lookupStrategy"/>
  4. </bean>

我们将会使用o.s.s.acls.jdbc.JdbcAclService,它是AclService的一个实现。这个实现是内置的并且使用我们这个练习上一步定义的数据库模式。JdbcAclService将会使用递归的SQL和事后处理机制来理解SID的等级关系,并确保等级关系的表述能够传递回AclEntryVoter。

JdbcAclService使用与嵌入式数据库定义时相同的JDBC dataSource,并代理给一个o.s.s.acls.jdbc.LookupStrategy的实现,而这个实现将真正负责数据库查询以及处理对ACL的请求。Spring Security提供的唯一LookupStrategy是o.s.s.acls.jdbc.BasicLookupStrategy,定义如下:

  1. <bean class="org.springframework.security.acls.jdbc.BasicLookupStrategy" id="lookupStrategy">
  2.   <constructor-arg ref="dataSource"/>
  3.   <constructor-arg ref="aclCache"/>
  4.   <constructor-arg ref="aclAuthzStrategy"/>
  5.   <constructor-arg ref="aclAuditLogger"/>
  6. </bean>

现在,BasicLookupStrategy是一个很复杂的家伙(beast)。记住,其目标是从数据库中将一系列的ObjectIdentity转换成实际可用的ACE列表。因为ObjectIdentity的声明可能是递归的,这会是一个很有挑战性的问题,并且对于高负荷使用的应用要考虑产生的SQL对数据库性能的影响。

【使用最小公分母进行查询。要注意的是BasicLookupStrategy要兼容所有的数据库,这通过严格坚持标准的ANSI SQL语法实现,要注意的是左[外]连接(left[outer]joins)。一些老的数据库(典型的如Oracle 8i)并不支持这种连接语法,所以要确保检查SQL的语法和结构是否兼容你特定的数据库。当然还有更高效依赖数据库的等级查询方法,这要使用非标准的SQL——如Oracle的CONNECT BY语句以及其它很多数据库(包括PostgreSQL和Microsoft SQL Server)的Common Table Expression(CTE)功能。正如我们在第四章使用JdbcDaoImpl UserDetailsService自定义模式的例子那样,在使用BasicLookupStrategy的时候也有属性暴露出来配置SQL。请查询Javadoc和源码本身来了解它们怎么使用,这样它们就能够在你自定义模式中正确使用。】

我们可以看到LookupStrategy需要引用与AclService相同的JDBC dataSoure。而另外三个引用将会把我们带到这个依赖链的最后。

o.s.s.acls.model.AclCache声明了一个接口依赖缓存ObjectIdentity到ACL的映射,这样就阻止大量(且代价高昂)的数据库查询。Spring Security只提供了一个AclCache的实现,即使用第三方库Ehcache。简单起见,在此时的配置中,我们忽略掉配置Ehcache的额外工作,替换为实现一个简单的,不进行任何操作的AclCache,在类com.packtpub.springsecurity.security.NullAclCache中:

  1. package com.packtpub.springsecurity.security;
  2. // imports omitted  
  3. public class NullAclCache implements AclCache {
  4.   @Override
  5.   public void clearCache() {  }
  6.   @Override
  7.   public void evictFromCache(Serializable arg0) { }
  8.   @Override
  9.   public void evictFromCache(ObjectIdentity arg0) {  }
  10.   @Override
  11.   public MutableAcl getFromCache(ObjectIdentity arg0) {
  12.     return null;
  13.   }
  14.   @Override
  15.   public MutableAcl getFromCache(Serializable arg0) {
  16.     return null;
  17.   }
  18.   @Override
  19.   public void putInCache(MutableAcl arg0) {  }
  20. }

这通过一个简单的bean声明来配置:

  1. <bean class="com.packtpub.springsecurity.security.NullAclCache"
  2. id="aclCache"/>

不要担心,我们将会在本章后面配置基于Ehcache的实现,但是现在我们首先想要关注配置ACL真正需要的组件。

BasicLookupStrategy所要处理的下一个依赖是o.s.s.acls.domain.AuditLogger接口的一个现实,它被BasicLookupStrategy用来进行审计ACL和ACE的查找。类似于AclCache接口,Spring Security只提供了一个实现,它简单地在控制台上记录log。我们将用一行的bean声明来配置它:

  1. <bean class="org.springframework.security.acls.domain.ConsoleAuditLogger"
  2.  id="aclAuditLogger"/>

最后一个要处理的依赖是o.s.s.acls.domain.AclAuthorizationStrategy接口的实现,它在从数据库加载ACL的时候并没有马上要提供的任何作用。相反,这个接口的实现负责确定运行时对ACL或ACE的修改是否允许,这要基于修改的类型。当我们涉及到易变的ACL时,会有更详细的介绍,因为这个逻辑过程有些复杂并且与我们初始化配置完成没有关系。最后的配置如下:

  1. <bean class="org.springframework.security.acls.domain.AclAuthorizationStrategyImpl"
  2. id="aclAuthzStrategy">
  3.   <constructor-arg>
  4.     <array>
  5.       <ref local="aclAdminAuthority"/>
  6.       <ref local="aclAdminAuthority"/>
  7.       <ref local="aclAdminAuthority"/>
  8.     </array>
  9.   </constructor-arg>
  10. </bean>
  11. <bean class="org.springframework.security.core.authority.GrantedAuthorityImpl"
  12.  id="aclAdminAuthority">
  13.   <constructor-arg value="ROLE_ADMIN"/>
  14. </bean>

你可能想知道重复引用的aclAdminAuthority是干什么的——AclAuthorizationStrategyImpl提供了三个特殊的GrantedAuthority名称来允许对ACL进行运行时的特定操作。我们将会本章后面涉及到。

我们最终完成了对Spring Security ACL内置实现的配置。接下来也是最后的一步就是需要我们插入简单的ACL和ACE到HSQL数据库中,并进行测试。

创建一个简单的ACL entry

回忆一下,我们简单的场景就是锁定JBCP Pets store的第一个分类,使得只有ROLE_ADMIN授权的原来才能查看。你可能会发现翻到前几页参考一下模式图对于理解我们要插入什么数据以及为什么会有帮助。

在WEB-INF下建立一个文件test-acl-data.sql,与其它我们在JBCP Pets中使用到的SQL文件放到一起。本节描述的所有SQL将会加到这个文件中——你可以基于我们提供的实例SQL随便体验并添加更多的测试用例——实际上,我们鼓励你使用实例数据进行体验。

首先,我们需要在ACL_CLASS中添加任意或所有的拥有ACL规则的域对象类——在我们的例子中,这就是我们的Category类:

  1. insert into acl_class (class) values ('com.packtpub.springsecurity.data.Category');

接下来,ACL_SID表插入SID,它将关联到ACE中。要记住的是,SID可以是角色或用户——在我们的应用中简单插入角色(注意principal列表明一个给定的行是否为单个用户):

  1. insert into acl_sid (principal, sid) values (false'ROLE_USER');
  2. insert into acl_sid (principal, sid) values (false'ROLE_ADMIN');

开始变得复杂的表是ACL_OBJECT_IDENTITY,它用来声明单个的域对象实例和它们的父对象(如果存在)以及拥有者的SID。我们插入拥有以下属性的一行数据:

域对象类型为Category(通过OBJECT_ID_CLASS列外键关联到ACL_CLASS);

域对象的PK为1(OBJECT_ID_IDENTITY);

拥有者SID为ROLE_ADMIN(通过外键OWNER_SID column关联到ACL_SID)。

对于主键为1的Category,插入一行的SQL如下:

  1. insert into acl_object_identity (object_id_class,object_id_
  2. identity,parent_object,owner_sid,entries_inheriting)
  3. select cl.id, 1null, sid.id, false
  4. from acl_class cl, acl_sid sid
  5. where cl.class='com.packtpub.springsecurity.data.Category' and sid.
  6. sid='ROLE_ADMIN';

要记住的是,在典型的场景下,拥有者SID应该为一个安全实体而不是一个角色。但是,对于ACL系统来说,两种类型的规则功能是一样的。

最后,我们要对这个只有ROLE_ADMIN角色才能访问的对象实例添加ACE:

  1. insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) select oi.id, 1, si.id, 1truetruetrue
  2. from acl_object_identity oi, acl_sid si
  3. where si.sid = 'ROLE_ADMIN';

这里的MASK列代表位掩码,它用来给指定对象在特定的SID上进行授权。我们将会在本章后面进行更详细的介绍——幸运的是,在这里它并不像听起来那么有用。

在SQL文件准备好之后,我们需要扩展<embedded-database>声明并添加最终的ACL测试数据文件到数据库启动中:

  1. <jdbc:embedded-database id="dataSource" type="HSQL">
  2. <!--additional SQL files omitted -->
  3.    <jdbc:script location="classpath:acl-schema.sql"/>
  4.    <jdbc:script location="classpath:test-acl-data.sql"/>
  5. </jdbc:embedded-database>

现在,我们能够启动应用并运行实例场景。你会发现不是管理员的任何用户试图访问第一个分类“Pet Apparel”,他们被拒绝访问。如果没有经过认证的用户试图访问这个分类,他们将会按照标准的AccessDeniedException处理(第六章已描述)并要求登录。

现在我们建立了基本的基于ACL的安全(尽管是一个很简单的场景)。接下来我们对这个过程中的概念进行更多的讲解,然后了解在使用Spring ACL之前两个要考虑的问题。

他的博客地址:http://lengyun3566.iteye.com

他的新浪微博:http://weibo.com/1920428940

本书源代码的地址:http://www.packtpub.com/support?nid=4435

来源: http://sishuok.com/forum/blogPost/list/3942.html