Acl的全称是Access Control List,俗称访问控制列表,是用以控制对象的访问权限的。其主要思想是将某个对象的某种权限授予给某个用户,或某种GrantedAuthority(可以简单的理解为某种角色),它们之间的关系都是多对多。如果某一个对象的某一操作是受保护的,那么在对该对象进行某种操作时就需要有对应的权限。
1.1 准备工作
使用Spring Security的Acl功能需要引入Acl相关的jar包。如果我们的应用是使用Maven构建的,则可以在应用的pom.xml文件中加入如下依赖。
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-acl</artifactId>
<version>${spring.security.version}</version>
</dependency>
此外,使用Spring Security的Acl时需要在数据库中建立四张表。在其官方文档中给出了一个基于数据库HSQLDB的建表语句。其脚本如下:
create table acl_sid (
id bigint generated by default as identity(start with 100) not null primary key,
principal boolean not null,
sid varchar_ignorecase(100) not null,
constraint unique_uk_1 unique(sid,principal) );
create table acl_class (
id bigint generated by default as identity(start with 100) not null primary key,
class varchar_ignorecase(100) not null,
constraint unique_uk_2 unique(class) );
create table acl_object_identity (
id bigint generated by default as identity(start with 100) not null primary key,
object_id_class bigint not null,
object_id_identity bigint not null,
parent_object bigint,
owner_sid bigint not null,
entries_inheriting boolean not null,
constraint unique_uk_3 unique(object_id_class,object_id_identity),
constraint foreign_fk_1 foreign key(parent_object)references acl_object_identity(id),
constraint foreign_fk_2 foreign key(object_id_class)references acl_class(id),
constraint foreign_fk_3 foreign key(owner_sid)references acl_sid(id) );
create table acl_entry (
id bigint generated by default as identity(start with 100) not null primary key,
acl_object_identity bigint not null,ace_order int not null,sid bigint not null,
mask integer not null,granting boolean not null,audit_success boolean not null,
audit_failure boolean not null,
constraint unique_uk_4 unique(acl_object_identity,ace_order),
constraint foreign_fk_4 foreign key(acl_object_identity)
references acl_object_identity(id),
constraint foreign_fk_5 foreign key(sid) references acl_sid(id) );
笔者使用的是Oracle数据库,其中没有boolean和主键自增功能,对于boolean类型都使用一位number表示。具体建表语句如下所示:
create table acl_sid (
id number not null primary key,
principal number(1) not null,
sid varchar(100) not null,
constraint unique_uk_1 unique(sid,principal) );
create table acl_class (
id number not null primary key,
class varchar(100) not null,
constraint unique_uk_2 unique(class) );
create table acl_object_identity (
id number not null primary key,
object_id_class number not null,
object_id_identity number not null,
parent_object number,
owner_sid number not null,
entries_inheriting number(1) not null,
constraint unique_uk_3 unique(object_id_class,object_id_identity),
constraint foreign_fk_1 foreign key(parent_object)references acl_object_identity(id),
constraint foreign_fk_2 foreign key(object_id_class)references acl_class(id),
constraint foreign_fk_3 foreign key(owner_sid)references acl_sid(id) );
create table acl_entry (
id number not null primary key,
acl_object_identity number not null,ace_order int not null,sid number not null,
mask number(3) not null,granting number(1) not null,audit_success number(1) not null,
audit_failure number(1) not null,
constraint unique_uk_4 unique(acl_object_identity,ace_order),
constraint foreign_fk_4 foreign key(acl_object_identity)
references acl_object_identity(id),
constraint foreign_fk_5 foreign key(sid) references acl_sid(id) );
新增记录时用于生成主键的sequence定义为:
create or replace sequence seq_acl_sid start with 1 increment by 1;
create or replace sequence seq_acl_class start with 1 increment by 1;
create or replace sequence seq_acl_object_identity start with 1 increment by 1;
create or replace sequence seq_acl_entry start with 1 increment by 1;
1.2 表功能介绍
如上所示,Spring Security的Acl功能需要使用到四张数据库表,分别为acl_sid、acl_class、acl_object_identity和acl_entry。
1.2.1 表acl_sid
表acl_sid的结构如下所示:
字段名 | 类型 | 说明 |
---|---|---|
id | number | 主键 |
sid | varchar | 字符串类型的sid |
principal | boolean | 是否用户 |
表acl_sid是用来保存Sid的。对于Acl而言,有两种类型的Sid,一种是基于用户的Sid,叫PrincipalSid;另一种是基于GrantedAuthority的Sid,叫GrantedAuthoritySid。acl_sid表的sid字段存放的是用户名或者是GrantedAuthority的字符串表示。prinpal是用来区分对应的Sid是用户还是GrantedAuthority的。正如在前文所描述的那样,Acl中对象的权限是用来授予给Sid的,Sid有用户和GrantedAuthority之分,所以我们的对象权限是可以用来授予给用户或GrantedAuthority的。
1.2.2 表acl_class
表acl_class的结构如下所示:
字段名 | 类型 | 说明 |
---|---|---|
id | number | 主键 |
class | varchar | 对象类型的全限定名 |
表acl_class是用来保存对象类型的,字段class中保存的是对应对象的全限定名。Acl需要使用它来区分不同的对象类型。
1.2.3 表acl_object_identity
表acl_object_identity的结构如下:
字段名 | 类型 | 描述 |
---|---|---|
id | number | 主键 |
object_id_class | number | 关联acl_class,表示对象类型 |
object_id_identity | number | 对象的主键,对于相同的class而言,其需要是唯一的。对象的主键默认需要是Long型,或者可以转换为Long型的对象,如Integer、Short等。 |
parent_object | number | 父对象的id,关联acl_object_identity |
owner_sid | number | 拥有者的sid,关联acl_sid |
entries_inheriting | boolean | 是否继承父对象的权限。打个比方,删除对象childObj需要有delete权限,用户A他没有childObj的delete权限,但是他有childObj的父对象parentObj的delete权限,当entries_inheriting为true时,用户A同样可以删除childObj。 |
表acl_object_identity是用来存放需要进行访问控制的对象的信息的。其保存的信息有对象的拥有者、对象的类型、对象的主键、对象的父对象和是否继承父对象的权限。
1.2.4 表acl_entry
表acl_entry的结构如下:
字段名 | 类型 | 说明 |
---|---|---|
id | number | 主键 |
acl_object_identity | number | 对应acl_object_identity的id |
ace_order | number | 所属Acl的权限顺序 |
sid | number | 对应acl_sid的id |
mask | number | 权限对应的掩码 |
granting | boolean | 是否授权 |
audit_success | boolean | 暂未发现其作用,Acl中有一个更新其值的方法,但未见被调用。 |
audit_failure | boolean |
表acl_entry是用于存放具体的权限信息的,从表结构我们也可以看出来,其描述的就是某个主体(Sid)对某个对象(acl_object_identity)是否(granting)拥有某种权限(mask)。当同一对象acl_object_identity在acl_entry表中拥有多条记录时,就会使用ace_order来标记对应的顺序,其对应于往Acl中插入AccessControlEntry时的位置,在进行权限判断时也是依靠ace_order的顺序来进行的,ace_order越小的越先进行判断。ace是Access Control Entry的简称。
1.3 Acl主要接口
对于Acl而言,有两块比较核心的功能,一块是往对应的数据库表里面插数据,另一块是从数据库表里面取出对应的数据进行权限鉴定。要了解这些功能我们先来了解Acl中用到的主要接口。
- Sid :可以用来表示一个principal,或者是一个GrantedAuthority。其对应的实现类有表示principal的PrincipalSid和表示GrantedAuthority的GrantedAuthoritySid。其信息会保存在acl_sid表中。
- ObjectIdentity :ObjectIdentity表示Spring Security Acl中一个域对象,其默认实现类是ObjectIdentityImpl。ObjectIdentity并不是直接与acl_object_identity表相对应的,真正与acl_object_identity表直接相对应的是Acl。
- Acl :每一个领域对象都会对应一个Acl,而且只会对应一个Acl。Acl是将Spring Security Acl中使用到的四个表串联起来的一个接口,其中会包含对象信息ObjectIdentity、对象的拥有者Sid和对象的访问控制信息AccessControlEntry。在Spring Security Acl中直接与acl_object_identity表相关联的是Acl接口,因为acl_object_identity表中的数据是通过保存Acl来进行的。一个Acl对应于一个ObjectIdentity,但是会包含有多个Sid和多个AccessControlEntry,即一个Acl表示所有Sid对一个ObjectIdentity的所有AccessControlEntry。Acl的默认实现类是AclImpl,该类实现Acl接口、MutableAcl接口、AuditableAcl接口和OwnershipAcl接口。
- AccessControlEntry :一个AccessControlEntry表示一条访问控制信息,一个Acl中可以拥有多个AccessControlEntry。在Spring Security Acl中很多地方会使用ACE来简单的表示AccessControlEntry这个概念,比如insertAce其实表示的就是insert AccessControlEntry。每一个AccessControlEntry表示对应的Sid对于对应的对象ObjectIdentity是否被授权某一项权限Permission,是否被授权将使用granting进行区分。AccessControlEntry对应表acl_entry。
- Permission :在Acl中使用一个bit掩码来表示一个Permission。Spring Security的Acl中默认使用的是BasePermission,其中已经定义了0-4五个bit掩码,分别对应于1、2、4、8、16,代表五种不同的Permission,分别是read (bit 0)、write (bit 1)、create (bit 2)、delete (bit 3)和administer (bit 4)。如果已经定义好的这五个bit掩码不能满足需求,我们可以对BasePermission进行扩展,也可以实现自己的Permission。Spring Security Acl默认的实现最多可以支持32个不同的掩码。
- AclService :AclService是用来通过ObjectIdentity解析Acl的,其默认实现类是JdbcAclService。JdbcAclService底层操作是通过LookupStrategy来进行的,LookupStrategy的默认实现是BasicLookupStrategy。
- MutableAclService :MutableAclService是用来对Acl进行持久化的,其默认实现类是JdbcMutableAclService。JdbcMutableAclService是继承自JdbcAclService的,所以我们可以同时通过JdbcMutableAclService对Acl进行读取和保存。如果我们希望自己来实现Acl信息的保存的话,我们也可以不使用该接口。
1.4 配置AclService
AclService是使用Spring Security Acl功能的主入口。这里选择一个既可以从数据库读取Acl信息,又可以保存Acl信息到数据库的JdbcMutableAclService做示例。
JdbcMutableAclService只有一个构造方法,它接收三个参数,DataSource、LookupStrategy和AclCache。其对应配置信息如下所示:
<bean id="aclService"
class="org.springframework.security.acls.jdbc.JdbcMutableAclService">
<constructor-arg ref="dataSource" />
<constructor-arg ref="lookupStrategy" />
<constructor-arg ref="aclCache" />
</bean>
配置JdbcMutableAclService有一点需要注意的地方,那就是其在与数据库进行交互的时候所基于的脚本是本文开始部分我们提到的那些脚本。其对应的数据库表的主键是自增的,所以在保存Acl时所给出的脚本中没有新增主键id。比如在新增sid时默认使用的脚本是“insert into acl_sid (principal, sid) values (?, ?)”,显然对于使用Oracle数据库作为示例的我们来说这条SQL是有问题的,因为新增的时候主键不能为空,所以如果我们需要使用JdbcMutableAclService来创建Acl的话我们得给JdbcMutableAclService指定新增记录时使用的脚本。这里我们将在新增的时候从之前建立好的Sequence获取值作为主键,示例如下:
<bean id="aclService"
class="org.springframework.security.acls.jdbc.JdbcMutableAclService">
<constructor-arg ref="dataSource" />
<constructor-arg ref="lookupStrategy" />
<constructor-arg ref="aclCache" />
<!-- 指定新增acl_sid的脚本 -->
<property name="insertSidSql"
value="insert into acl_sid(id, principal, sid) values (seq_acl_sid.nextval, ?, ?)" />
<!-- 指定新增acl_class的脚本 -->
<property name="insertClassSql"
value="insert into acl_class(id, class) values (seq_acl_class.nextval, ?)" />
<!-- 指定新增acl_object_identity的脚本 -->
<property name="insertObjectIdentitySql"
value="insert into acl_object_identity(id, object_id_class, object_id_identity, owner_sid, entries_inheriting) values(seq_acl_object_identity.nextval, ?, ?, ?, ?)" />
<!-- 指定新增acl_entry的脚本 -->
<property name="insertEntrySql"
value="insert into acl_entry(id, acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) values (seq_acl_entry.nextval, ?, ?, ?, ?, ?, ?, ?)" />
</bean>
除了上述四个SQL之外,我们还需要指定两个属性对应的查询SQL,sidIdentityQuery和classIdentityQuery。因为JdbcMutableAclService在创建Acl时,如果当前用户在acl_sid表中不存在或当前对象类型在acl_class表中不存在,其会先将对应的信息存入acl_sid表和acl_class表,然后需要取出刚刚新增的acl_sid的主键和acl_class的主键以往acl_object_identity表中插入数据,对应acl_object_identity表中的owner_sid和object_id_class字段。这两个属性的默认值是“call identity()”,显然对于Oracle数据库来说这是行不通的,所以我们需要自己指定它们。这里我们通过对应Sequence的当前值来获取刚刚新增的记录的主键。如:
<bean id="aclService"
class="org.springframework.security.acls.jdbc.JdbcMutableAclService">
<constructor-arg ref="dataSource" />
<constructor-arg ref="lookupStrategy" />
<constructor-arg ref="aclCache" />
<!-- 指定新增acl_sid的脚本 -->
<property name="insertSidSql"
value="insert into acl_sid(id, principal, sid) values (seq_acl_sid.nextval, ?, ?)" />
<!-- 指定新增acl_class的脚本 -->
<property name="insertClassSql"
value="insert into acl_class(id, class) values (seq_acl_class.nextval, ?)" />
<!-- 指定新增acl_object_identity的脚本 -->
<property name="insertObjectIdentitySql"
value="insert into acl_object_identity(id, object_id_class, object_id_identity, owner_sid, entries_inheriting) values(seq_acl_object_identity.nextval, ?, ?, ?, ?)" />
<!-- 指定新增acl_entry的脚本 -->
<property name="insertEntrySql"
value="insert into acl_entry(id, acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) values (seq_acl_entry.nextval, ?, ?, ?, ?, ?, ?, ?)" />
<!-- 查询刚刚新增的acl_sid的主键的SQL -->
<property name="sidIdentityQuery" value="select seq_acl_sid.currval from dual" />
<!-- 查询刚刚新增的acl_class的主键的SQL -->
<property name="classIdentityQuery" value="select seq_acl_class.currval from dual" />
</bean>
1.4.1 配置DataSource
配置数据源这个没什么好说的,大家都见惯了,为保持文章的完整性,我这里还是把它列一下。直接上代码:
<context:property-placeholder location="/WEB-INF/config/jdbc.properties" />
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>
1.4.2 配置LookupStrategy
LookupStrategy是用来通过ObjectIdentity解析为对应的Acl的。Spring Security Acl中的默认实现类是BasicLookupStrategy,其的构造需要接收四个参数。
<bean id="lookupStrategy"
class="org.springframework.security.acls.jdbc.BasicLookupStrategy">
<constructor-arg ref="dataSource" />
<constructor-arg ref="aclCache" />
<constructor-arg ref="aclAuthorizationStrategy" />
<constructor-arg ref="grantingStrategy" />
</bean>
1.4.3 配置AclAuthorizationStrategy
aclAuthorizationStrategy是在构造Acl的实现类AclImpl时必须给定的一个参数,其会用来在对Acl进行某些操作时检查当前用户是否具有对应的权限。AclAuthorizationStrategy的默认实现类是AclAuthorizationStrategyImpl,其构造需要接收一个或三个GrantedAuthority参数,用来对Acl进行相关操作时所需要的权限,包括更改Acl对应对象的所有者需要的权限、更改Acl中包含的某个AccessControlEntry的audit信息(对应acl_entry表中的is_audit_success和is_audit_failure字段)需要的权限以及其它如增、删、改Acl中所包含的AccessControlEntry等需要的权限。这些权限的鉴定是我们在操作Acl时由Spring Security Acl内部进行判断的,我们只需要在这里定义就好。当Acl对应的所有者对Acl进行操作时,不管其是否拥有指定需要的权限,除了改变audit信息之外的所有操作默认都是被允许的。当只有一个参数时表示三者共用一个GrantedAuthority。
<bean id="aclAuthorizationStrategy"
class="org.springframework.security.acls.domain.AclAuthorizationStrategyImpl">
<constructor-arg>
<list>
<bean class="org.springframework.security.core.authority.SimpleGrantedAuthority">
<constructor-arg value="ROLE_ADMIN" /><!-- 改变所有权需要的权限 -->
</bean>
<bean class="org.springframework.security.core.authority.SimpleGrantedAuthority">
<constructor-arg value="gaModifyAuditing" /><!-- 改变授权需要的权限 -->
</bean>
<bean class="org.springframework.security.core.authority.SimpleGrantedAuthority">
<constructor-arg value="gaGeneralChanges" /><!-- 改变其它信息所需要的权限 -->
</bean>
</list>
</constructor-arg>
</bean>
1.4.4 配置grantingStrategy
grantingStrategy对应类型为PermissionGrantingStrategy接口,其中只定义了一个isGranted方法,用于判断基于指定的Permission列表和Sid列表指定的Acl是否被授予了访问权限。其默认实现类是DefaultPermissionGrantingStrategy。DefaultPermissionGrantingStrategy对于isGranted的实现逻辑是依次遍历Permission列表、Sid列表和Acl中包含的AccessControlEntry列表,找到第一个三者能够匹配的AccessControlEntry的isGranting(对应acl_entry表的granting字段)作为isGranted的返回结果。如果在当前Acl中没有找到匹配的AccessControlEntry,同时Acl对应的entriesInheriting为true时将继续使用父级的Acl进行匹配,并依次进行,如果都没有匹配到,则将抛出异常。
<bean id="grantingStrategy"
class="org.springframework.security.acls.domain.DefaultPermissionGrantingStrategy">
<constructor-arg>
<bean class="org.springframework.security.acls.domain.ConsoleAuditLogger" />
</constructor-arg>
</bean>
1.4.5 配置AclCache
AclCache是用来缓存Acl信息的,Spring Security Acl中对于AclCache的默认实现是基于Ehcache的实现类EhCacheBasedAclCache。
<bean id="aclCache"
class="org.springframework.security.acls.domain.EhCacheBasedAclCache">
<constructor-arg ref="cache" /><!-- 对应于Ehcache -->
<constructor-arg ref="grantingStrategy" />
<constructor-arg ref="aclAuthorizationStrategy" />
</bean>
<!-- 定义一个Ehcache -->
<bean id="cache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
<property name="cacheName" value="aclCache" />
<property name="cacheManager" ref="aclCacheManager" />
</bean>
<!-- 定义CacheManager -->
<bean id="aclCacheManager"
class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<!-- 指定配置文件的位置 -->
<property name="configLocation" value="/WEB-INF/config/ehcache.xml" />
<!-- 指定新建的CacheManager的名称 -->
<property name="cacheManagerName" value="aclCacheManager" />
</bean>
为保持本文的完整性,这里贴出上述使用到的配置文件ehcache.xml的内容。具体如下所示:
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
maxBytesLocalHeap="100M">
<diskStore path="d:\\ehcache" />
<defaultCache maxEntriesLocalHeap="200" />
<cache name="aclCache" maxBytesLocalHeap="50M" maxBytesLocalDisk="5G"
timeToIdleSeconds="120" timeToLiveSeconds="600" />
</ehcache>
关于Ehcache的更多内容不在本文讨论范围之内,有需要的读者可以参考官方文档,也可以参考我的另一个关于Ehcache的系列文章。
至此,关于AclService配置的内容就讲完了,AclService配置的完整内容如下:
<bean id="aclService"
class="org.springframework.security.acls.jdbc.JdbcMutableAclService">
<constructor-arg ref="dataSource" />
<constructor-arg ref="lookupStrategy" />
<constructor-arg ref="aclCache" />
<!-- 指定新增acl_sid的脚本 -->
<property name="insertSidSql"
value="insert into acl_sid(id, principal, sid) values (seq_acl_sid.nextval, ?, ?)" />
<!-- 指定新增acl_class的脚本 -->
<property name="insertClassSql"
value="insert into acl_class(id, class) values (seq_acl_class.nextval, ?)" />
<!-- 指定新增acl_object_identity的脚本 -->
<property name="insertObjectIdentitySql"
value="insert into acl_object_identity(id, object_id_class, object_id_identity, owner_sid, entries_inheriting) values(seq_acl_object_identity.nextval, ?, ?, ?, ?)" />
<!-- 指定新增acl_entry的脚本 -->
<property name="insertEntrySql"
value="insert into acl_entry(id, acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) values (seq_acl_entry.nextval, ?, ?, ?, ?, ?, ?, ?)" />
<!-- 查询刚刚新增的acl_sid的主键的SQL -->
<property name="sidIdentityQuery" value="select seq_acl_sid.currval from dual" />
<!-- 查询刚刚新增的acl_class的主键的SQL -->
<property name="classIdentityQuery" value="select seq_acl_class.currval from dual" />
</bean>
<bean id="lookupStrategy"
class="org.springframework.security.acls.jdbc.BasicLookupStrategy">
<constructor-arg ref="dataSource" />
<constructor-arg ref="aclCache" />
<constructor-arg ref="aclAuthorizationStrategy" />
<constructor-arg ref="grantingStrategy" />
</bean>
<bean id="aclCache"
class="org.springframework.security.acls.domain.EhCacheBasedAclCache">
<constructor-arg ref="cache" /><!-- 对应于Ehcache -->
<constructor-arg ref="grantingStrategy" />
<constructor-arg ref="aclAuthorizationStrategy" />
</bean>
<!-- 定义一个Ehcache -->
<bean id="cache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
<property name="cacheName" value="aclCache" />
<property name="cacheManager" ref="aclCacheManager" />
</bean>
<!-- 定义CacheManager -->
<bean id="aclCacheManager"
class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<!-- 指定配置文件的位置 -->
<property name="configLocation" value="/WEB-INF/config/ehcache.xml" />
<!-- 指定新建的CacheManager的名称 -->
<property name="cacheManagerName" value="aclCacheManager" />
</bean>
<bean id="aclAuthorizationStrategy"
class="org.springframework.security.acls.domain.AclAuthorizationStrategyImpl">
<constructor-arg>
<list>
<bean class="org.springframework.security.core.authority.SimpleGrantedAuthority">
<constructor-arg value="ROLE_ADMIN" /><!-- 改变所有权需要的权限 -->
</bean>
<bean class="org.springframework.security.core.authority.SimpleGrantedAuthority">
<constructor-arg value="gaModifyAuditing" /><!-- 改变授权需要的权限 -->
</bean>
<bean class="org.springframework.security.core.authority.SimpleGrantedAuthority">
<constructor-arg value="gaGeneralChanges" /><!-- 改变其它信息所需要的权限 -->
</bean>
</list>
</constructor-arg>
</bean>
<bean id="grantingStrategy"
class="org.springframework.security.acls.domain.DefaultPermissionGrantingStrategy">
<constructor-arg>
<bean class="org.springframework.security.acls.domain.ConsoleAuditLogger" />
</constructor-arg>
</bean>
1.5 使用AclService
配置好AclService之后我们就可以使用该JdbcMutableAclService来创建、更新和查找Acl了。在Spring Security Acl中Acl接口的默认实现类是AclImpl,该类实现Acl接口、MutableAcl接口、AuditableAcl接口和OwnershipAcl接口,当有必要在这几种接口之间切换时可以任意切换。
1.5.1 创建Acl
可以通过调用JdbcMutableAclService的createAcl()方法来创建一个Acl,其对应返回的是一个MutableAcl,该方法接收一个ObjectIdentity作为参数。在创建的时候如果ObjectIdentity对应的类型在acl_class表中不存在,则会把ObjectIdentity对应的类型添加到acl_class表中;如果当前用户对应的Sid在acl_sid表中不存在则会将其添加到acl_sid中。最后会将ObjectIdentity保存到acl_object_identity表中。正如在本文开始部分所描述的那样,一个Acl对应于一个ObjectIdentity,创建Acl就是创建ObjectIdentity的过程。在这三部分都完成之后,会重新从数据库查询出一个Acl,只是此时该Acl对应的AccessControlEntry列表为空。通常如果我们的对象是需要利用Acl进行访问控制的话,那么我们可以在创建该对象的时候一并创建该对象对应的Acl。
@Autowired
private MutableAclService aclService;
public void addUser(User user) {
...
//1、构建一个ObjectIdentity
ObjectIdentity oi = new ObjectIdentityImpl(User.class, user.getId());
//2、创建一个Acl、此时会如果对应的信息不存在会依次创建,如当前用户对应的Sid、ObjectIdentity对应于acl_class表中的类型
//最后是往acl_object_identity中插入对应的数据
MutableAcl acl = aclService.createAcl(oi);
...
}
ObjectIdentityImpl拥有多个构造方法,具体可以参考Spring Security的API文档。
1.5.2 查找Acl
通过AclService的系列readAclById()方法可以通过给定的ObjectIdentity查找对应的Acl。此外通过findChildren()方法可以查找指定ObjectIdentity的子ObjectIdentity。关于这些方法的具体信息可以参考Spring Security的API文档。以下是一个简单的示例。
ObjectIdentity oi = new ObjectIdentityImpl(User.class, user.getId());
//获取ObjectIdentity对应的Acl
MutableAcl acl = (MutableAcl) aclService.readAclById(oi);
1.5.3 更新Acl
Acl的更新主要是对应AccessControlEntry的更新,即对AccessControlEntry的增、删、改;此外还包括对Acl对应的ObjectIdentity信息的变更,如更改所有者、父子关系等。
如下是一些更新Acl的示例,需要注意的是在调用MutableAclService的updateAcl()方法将对应信息同步到数据库之前,对Acl所做的所有修改都只是在内存中的。使用updateAcl()更新Acl信息到数据库时,其底层实现会先将数据库中所有对应的AccessControlEntry都删除,然后再将内存中的AccessControlEntry列表保存到数据库中。以下是其实现代码。
public MutableAcl updateAcl(MutableAcl acl) throws NotFoundException {
Assert.notNull(acl.getId(), "Object Identity doesn't provide an identifier");
// Delete this ACL's ACEs in the acl_entry table
deleteEntries(retrieveObjectIdentityPrimaryKey(acl.getObjectIdentity()));
// Create this ACL's ACEs in the acl_entry table
createEntries(acl);
// Change the mutable columns in acl_object_identity
updateObjectIdentity(acl);
// Clear the cache, including children
clearCacheIncludingChildren(acl.getObjectIdentity());
// Retrieve the ACL via superclass (ensures cache registration, proper retrieval etc)
return (MutableAcl) super.readAclById(acl.getObjectIdentity());
}
1.5.3.1 添加AccessControlEntry
添加AccessControlEntry是通过MutableAcl的insertAce()方法进行的,该方法的定义如下所示:
/**
*
* @param atIndexLocation 添加的位置,对应于acl_entry表中的ace_order字段
* @param permission 对应的Permission
* @param sid 对应Sid
* @param granting 是否赋予
*/
void insertAce(int atIndexLocation, Permission permission, Sid sid, boolean granting)
参数atIndexLocation对应的是需要插入的AccessControlEntry在Acl对应的AccessControlEntry列表(java.util.List类型)中的位置,对应于acl_entry表中ace_order,而一个Acl代表其对应ObjectIdentity的所有关联Sid关联的所有AccessControlEntry,这个ace_order是在这个范围内的order。以下是一个添加AccessControlEntry的示例:
MutableAcl acl = ...;
//基于principal的Sid
Sid sid = new PrincipalSid(SecurityContextHolder.getContext().getAuthentication());
Permission p = BasePermission.ADMINISTRATION;//管理员权限
//将当前Acl的管理员权限赋予给指定的Sid
acl.insertAce(acl.getEntries().size(), p, sid, true); //添加AccessControlEntry到内存
//保存到数据库
acl = aclService.updateAcl(acl);
上述的Sid,也可以是一个GrantedAuthoritySid,当把一个Acl的某Permission赋予给一个GrantedAuthoritySid时表示拥有该GrantedAuthority的用户都将拥有对应的Permission。如:
MutableAcl acl = ...;
//基于GrantedAuthority的Sid
Sid sid = new GrantedAuthoritySid("ROLE_ADMIN");
Permission p = BasePermission.ADMINISTRATION;//管理员权限
//将当前Acl的管理员权限赋予给指定的Sid
acl.insertAce(acl.getEntries().size(), p, sid, true); //添加AccessControlEntry到内存
//保存到数据库
acl = aclService.updateAcl(acl);
1.5.3.2 删除AccessControlEntry
通过调用MutableAcl的deleteAce(int aceIndex)方法可以删除Acl中指定位置的AccessControlEntry,aceIndex是从0开始的,底层是使用的List的remove(int index)方法。
ObjectIdentity oi = new ObjectIdentityImpl(User.class, user.getId());
//获取ObjectIdentity对应的Acl
MutableAcl acl = (MutableAcl) aclService.readAclById(oi);
acl.deleteAce(0);//内存中删除第一个AccessControlEntry
aclService.updateAcl(acl);//同步到数据库
1.5.3.3 更新AccessControlEntry
通过MutableAcl的updateAce()可以更新指定位置的AccessControlEntry的Permission。
ObjectIdentity oi = new ObjectIdentityImpl(User.class, user.getId());
//获取ObjectIdentity对应的Acl
MutableAcl acl = (MutableAcl) aclService.readAclById(oi);
acl.updateAce(0, BasePermission.CREATE);//内存中更新第一个AccessControlEntry对应的Permission
aclService.updateAcl(acl);//同步到数据库
1.5.3.4 修改所有者
可以通过Acl的getOwner()方法获取Acl对应ObjectIdentity的拥有者Sid。通过MutableAcl的setOwner()方法可以在内存中更新对应Acl的拥有者。
ObjectIdentity oi = new ObjectIdentityImpl(User.class, user.getId());
//获取ObjectIdentity对应的Acl
MutableAcl acl = (MutableAcl) aclService.readAclById(oi);
acl.setOwner(new PrincipalSid("user"));//内存中更改拥有者
aclService.updateAcl(acl);//同步到数据库
1.5.3.5 修改父Acl
通过Acl的getParentAcl()方法可以获取到Acl对应ObjectIdentity对应的父ObjectIdentity对应的Acl。通过MutableAcl的setParent()方法可以在内存中修改Acl对应的父Acl,即修改Acl对应ObjectIdentity对应的父ObjectIdentity。
ObjectIdentity oi = new ObjectIdentityImpl(User.class, user.getId());
//获取ObjectIdentity对应的Acl
MutableAcl acl = (MutableAcl) aclService.readAclById(oi);
Acl newParent = ...;//以某种方式获取到Acl
acl.setParent(newParent);//内存中更改父Acl
aclService.updateAcl(acl);//同步到数据库
1.5.3.6 修改继承策略
通过Acl的isEntriesInheriting()可以获取到当前Acl对应ObjectIdentity的继承策略,创建Acl时该值默认为true。通过MutableAcl的setEntriesInheriting()方法可以在内存中修改该Acl对应ObjectIdentity的继承策略。
ObjectIdentity oi = new ObjectIdentityImpl(User.class, user.getId());
//获取ObjectIdentity对应的Acl
MutableAcl acl = (MutableAcl) aclService.readAclById(oi);
acl.setEntriesInheriting(false);//内存中修改为不从父Acl继承AccessControlEntry
aclService.updateAcl(acl);//同步到数据库
1.5.4 删除Acl
当我们删除对象的时候应该连同对应的Acl也一起删除。使用MutableAclService的deleteAcl(ObjectIdentity oi, boolean deleteChildren)方法可以删除指定ObjectIdentity对应的Acl,deleteChildren表示是否连同子ObjectIdentity对应的Acl也一起删除。deleteAcl将删除对应的ObjectIdentity,以及对应的AccessControlEntry,即其会删除acl_object_identity表和acl_entry表中与当前Acl对应的ObjectIdentity相关的记录。
ObjectIdentity objectIdentity = new ObjectIdentityImpl(User.class, id);
aclService.deleteAcl(objectIdentity, true);
1.6 注入到AclPermissionEvaluator
AclPermissionEvaluator是PermissionEvaluator的一个实现类。在之前关于使用基于表达式的权限控制一文中有提到过,PermissionEvaluator是为表达式hasPermission提供支持的。此外,PermissionEvaluator还为标签accesscontrollist提供支持。本节将就使用AclPermissionEvaluator支持在方法上使用@PreAuthorize进行权限控制时使用表达式hasPermission做一个简单讲解。
AclPermissionEvaluator的构造需要接收一个AclService参数,在进行权限鉴定时其需要通过AclService获取到对应对象对应的Acl,然后判断该Acl中是否具有指定的Sid和指定的Permission。
对于方法使用hasPermission表达式进行权限鉴定时需要做两个事情,首先需要指定global-method-security的pre-post-annotations="enabled"。其次需要手工定义DefaultMethodSecurityExpressionHandler并指定其permissionEvaluator为我们定义的AclPermissionEvaluator。
<security:global-method-security pre-post-annotations="enabled">
<security:expression-handler ref="expressionHandler"/>
</security:global-method-security>
<bean id="expressionHandler" class="org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
<property name="permissionEvaluator" ref="aclPermissionEvaluator"/>
</bean>
<bean id="aclPermissionEvaluator"
class="org.springframework.security.acls.AclPermissionEvaluator">
<constructor-arg ref="aclService" />
</bean>
定义后之后我们就可以在方法上使用@PreAuthorize和hasPermission表达式了。关于PermissionEvaluator和hasPermission的更多介绍可以参考《基于表达式的权限控制》一文。hasPermission有两种用法,一种是直接传一个对象和对应需要的权限,如hasPermission(object,permission);另一种是传对象的id、对应类型和需要的权限,如hasPermission(targetId,targetType,permission)。传入的Permission是Spring Security Acl中的Permission接口实现类,而不是Spring Security的GrantedAuthority。传入的permission参数可以是一个Permission对象或Permission对象数组,也可以是一个整数或字符串。当传入的permission是整数或字符串时将由AclPermissionEvaluator的PermissionFactory进行解析,AclPermissionEvaluator默认拥有的PermissionFactory是DefaultPermissionFactory,其会将整形或字符串类型的permission解析成对应的BasePermission。如前所述,BasePermission中定义了五个BasePermission,其对应的名称和掩码分别为:READ (1)、WRITE (2)、CREATE (4)、DELETE (8)和ADMINISTER (16)。当permission使用字符串时我们只能使用这五种字符串,不区分大小写,表示当前用户或其所拥有的GrantedAuthority必须拥有指定对象的指定Permission才允许访问。但是当使用掩码时我们可以使用1、2、4、8和16。
接下来将简单的介绍一个使用@PreAuthorize和hasPermission表达式在方法上进行权限控制的示例。
@PreAuthorize("hasPermission(#id, 'com.spring.security.entity.User', 1)")
public User find(int id) {
User user = new User();
user.setId(id);
return user;
}
上述配置即表示调用find方法查看指定id对应的User对象时必须拥有掩码为1对应的Permission才行,或者在继续策略为true时拥有指定id父对象掩码为1对应的Permission也行。这时哪怕你是该User对象的拥有者,或者你拥有ADMINISTER权限,如果你没有对应的READ权限,你也不能访问该方法。如果觉得这种实现不符合你的要求,你可以实现自己的PermissionGrantingStrategy,然后将实现类bean注入到BasicLookupStrategy和EhcacheBasedAclCache中,这样在判断一个用户是否具有指定Acl的指定Permission时就可以使用自己的逻辑了。
上面的定义如果改成permission参数直接使用对象,可以这样定义:
@PreAuthorize("hasPermission(#id, 'com.spring.security.entity.User', T(org.springframework.security.acls.domain.BasePermission).READ)")
public User find(int id) {
User user = new User();
user.setId(id);
return user;
}
当permission参数定义为一个Permission数组时,会根据顺序依次匹配当前用户在指定的Acl中是否拥有对应Permission的AccessControlEntry,如果拥有则以第一个匹配到的AccessControlEntry的granting属性作为判断结果,没有匹配到还可以根据继承策略决定是否利用父级Acl进行匹配,都没匹配到就会抛异常了。
默认使用的BasePermission中只定义了五种Permission,如果这不能满足你的要求,那么我们可以实现自己的Permission,然后把它们注册到DefaultPermissionFactory中,并手工将该DefaultPermissionFactory注入到AclPermissionEvaluator中。前文已经说过AclPermissionEvaluator中使用的PermissionFactory默认是DefaultPermissionFactory,DefaultPermissionFactory中默认只注册了BasePermission中对应的五种Permission。以下是一个扩展Permission的简单示例。
首先实现自己的Permission类,这里简单的定义一个自己的类,然后继承BasePermission类。
public class BasePermissionExt extends BasePermission {
/**
* serialVersionUID
*/
private static final long serialVersionUID = 1L;
public BasePermissionExt(int mask) {
super(mask);
}
public BasePermissionExt(int mask, char code) {
super(mask, code);
}
}
然后在DefaultPermissionFactory中注册基于我们自己实现的Permission对象,并将该DefaultPermissionFactory注入到AclPermissionEvaluator中。
<bean id="aclPermissionEvaluator"
class="org.springframework.security.acls.AclPermissionEvaluator">
<constructor-arg ref="aclService" />
<!-- 手工注册DefaultPermissionFactory和其中的Permission -->
<property name="permissionFactory">
<bean class="org.springframework.security.acls.domain.DefaultPermissionFactory">
<constructor-arg>
<map>
<entry key="READ">
<bean class="com.xxx.spring.security.BasePermissionExt">
<constructor-arg value="1"/>
</bean>
</entry>
<entry key="WRITE">
<bean class="com.xxx.spring.security.BasePermissionExt">
<constructor-arg value="2"/>
</bean>
</entry>
</map>
</constructor-arg>
</bean>
</property>
</bean>
此外,还可以将我们的BasePermissionExt改成如下这样:
public class BasePermissionExt extends BasePermission {
/**
* serialVersionUID
*/
private static final longserialVersionUID = 1L;
public static final Permission READ = new BasePermissionExt(1 << 0, 'R'); // 1
public static final Permission WRITE = new BasePermissionExt(1 << 1, 'W'); // 2
public static final Permission CREATE = new BasePermissionExt(1 << 2, 'C'); // 4
public static final Permission DELETE = new BasePermissionExt(1 << 3, 'D'); // 8
public static final Permission ADMINISTRATION = new BasePermissionExt(1 << 4, 'A'); // 16
public BasePermissionExt(int mask) {
super(mask);
}
public BasePermissionExt(int mask, char code) {
super(mask, code);
}
}
然后通过传入Class参数来构造DefaultPermissionFactory。这个时候会将对应Class中所有类型为Permission的字段分别以字段名和字段值Permission对应的掩码为Key,以字段值Permission为Value进行注册。
<bean id="aclPermissionEvaluator"
class="org.springframework.security.acls.AclPermissionEvaluator">
<constructor-arg ref="aclService" />
<!-- 手工注册DefaultPermissionFactory和其中的Permission -->
<property name="permissionFactory">
<bean class="org.springframework.security.acls.domain.DefaultPermissionFactory">
<constructor-arg value="com.spring.security.BasePermissionExt"/>
</bean>
</property>
</bean>