Spring Security中可以使用Acegi-1.x时代的普通配置方式,也可以使用从2.0时代才出现的命名空间配置方式,实际上这两者实现的功能是完全一致的,只是新的命名空间配置方式可以把原来需要几百行的配置压缩成短短的几十行。我们的教程中都会使用命名空间的方式进行配置,凡事务求最简。
为了在项目中使用Spring Security控制权限,首先要在web.xml中配置过滤器,这样我们就可以控制对这个项目的每个请求了。
<filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
所有的用户在访问项目之前,都要先通过Spring Security的检测,这从第一时间把没有授权的请求排除在系统之外,保证系统资源的安全。关于过滤器配置的更多讲解可以参考http://www.family168.com/tutorial/jsp/html/jsp-ch-07.html#jsp-ch-07-03-01。
在applicationContext.xml中使用Spring Security提供的命名空间进行配置。
<?xml version="1.0" encoding="UTF-8"?> <beans:beans xmlns="http://www.springframework.org/schema/security"xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.4.xsd"> <http auto-config='true'>
<intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
<intercept-url pattern="/**" access="ROLE_USER" /> </http> <authentication-provider> <user-service> <user name="admin" password="admin" authorities="ROLE_USER, ROLE_ADMIN" />
<user name="user" password="user" authorities="ROLE_USER" /> </user-service> </authentication-provider> </beans:beans>
声明在xml中使用Spring Security提供的命名空间。 | |
http部分配置如何拦截用户请求。auto-config='true'将自动配置几种常用的权限控制机制,包括form, anonymous, rememberMe。 | |
我们利用intercept-url来判断用户需要具有何种权限才能访问对应的url资源,可以在pattern中指定一个特定的url资源,也可以使用通配符指定一组类似的url资源。例子中定义的两个intercepter-url,第一个用来控制对/admin.jsp的访问,第二个使用了通配符/**,说明它将控制对系统中所有url资源的访问。 在实际使用中,Spring Security采用的是一种就近原则,就是说当用户访问的url资源满足多个intercepter-url时,系统将使用第一个符合条件的intercept-url进行权限控制。在我们这个例子中就是,当用户访问/admin.jsp时,虽然两个intercept-url都满足要求,但因为第一个intercept-url排在上面,所以Spring Security会使用第一个intercept-url中的配置处理对/admin.jsp的请求,也就是说,只有那些拥有了ROLE_ADMIN权限的用户才能访问/admin.jsp。 access指定的权限部分比较有趣,大家可以注意到这些权限标示符都是以ROLE_开头的,实际上这与Spring Security中的Voter机制有着千丝万缕的联系,只有包含了特定前缀的字符串才会被Spring Security处理。目前来说我们只需要记住这一点就可以了,在教程以后的部分中我们会详细讲解Voter的内容。 | |
user-service中定义了两个用户,admin和user。为了简便起见,我们使用明文定义了两个用户对应的密码,这只是为了当前演示的方便,之后的例子中我们会使用Spring Security提供的加密方式,避免用户密码被他人窃取。 最最重要的部分是authorities,这里定义了这个用户登陆之后将会拥有的权限,它与上面intercept-url中定义的权限内容一一对应。每个用户可以同时拥有多个权限,例子中的admin用户就拥有ROLE_ADMIN和ROLE_USER两种权限,这使得admin用户在登陆之后可以访问ROLE_ADMIN和ROLE_USER允许访问的所有资源。 与之对应的是,user用户就只拥有ROLE_USER权限,所以他只能访问ROLE_USER允许访问的资源,而不能访问ROLE_ADMIN允许访问的资源。 |
因为Spring Security是建立在Spring的基础之上的,所以web.xml中除了需要配置我们刚刚提到的过滤器,还要加上加载Spring的相关配置。最终得到的web.xml看起来像是这样:
<?xml version="1.0" encoding="UTF-8"?> <web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext*.xml</param-value> </context-param> <filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> </web-app>
演示不同权限的用户登陆之后可以访问不同的资源,我们为项目添加了两个jsp文件,admin.jsp和index.jsp。其中admin.jsp只有那些拥有ROLE_ADMIN权限的用户才能访问,而index.jsp只允许那些拥有ROLE_USER权限的用户才能访问。
最终我们的整个项目会变成下面这样:
+ ch001/ + src/ + main/ + resources/ * applicationContext.xml + webapp/ + WEB-INF/ * web.xml * admin.jsp * index.jsp + test/ + resources/ * pom.xml
首先确保自己安装了Maven2。如果之前没用过Maven2,可以参考我们的Maven2教程http://www.family168.com/oa/maven2/html/index.html。
安装好Maven2之后,进入ch001目录,然后执行mvn。
信息: Root WebApplicationContext: initialization completed in 1578 ms 2009-05-28 11:37:50.171::INFO: Started SelectChannelConnector@0.0.0.0:8080 [INFO] Started Jetty Server [INFO] Starting scanner at interval of 10 seconds.
等到项目启动完成后。打开浏览器访问http://localhost:8080/ch001/就可以看到登陆页面。
这个简陋的页面是Spring Security自动生成的,一来为了演示的方便,二来避免用户自己编写登陆页面时犯错,Spring Security为了避免可能出现的风险,连测试用的登录页面都自动生成出来了。在这里我们就省去编写登陆页面的步骤,直接使用默认生成的登录页面进行演示吧。
首先让我们输入一个错误用的用户名或密码,这里我们使用test/test,当然这个用户是不存在的,点击提交之后我们会得到这样一个登陆错误提示页面。
如果输入的是正确的用户名和密码,比如user/user,系统在登陆成功后会默认跳转到index.jsp。
这时我们可以点击admin.jsp链接访问admin.jsp,也可以点击logout进行注销。
如果点击了logout,系统会注销当前登陆的用户,然后跳转至登陆页面。如果点击了admin.jsp链接就会显示如下页面。
很遗憾,user用户是无法访问/admin.jsp这个url资源的,这在上面的配置文件中已经有过深入的讨论。我们在这里再简要重复一遍:user用户拥有ROLE_USER权限,但是/admin.jsp资源需要用户拥有ROLE_ADMIN权限才能访问,所以当user用户视图访问被保护的/admin.jsp时,Spring Security会在中途拦截这一请求,返回拒绝访问页面。
为了正常访问admin.jsp,我们需要先点击logout注销当前用户,然后使用admin/admin登陆系统,然后再次点击admin.jsp链接就会显示出admin.jsp中的内容。
根据我们之前的配置,admin用户拥有ROLE_ADMIN和ROLE_USER两个权限,因为他拥有ROLE_USER权限,所以可以访问/index.jsp,因为他拥有ROLE_ADMIN权限,所以他可以访问/admin.jsp。
至此,我们很高兴的宣布,咱们已经正式完成,并运行演示了一个最简单的由Spring Security保护的web系统,下一步我们会深入讨论Spring Security为我们提供的其他保护功能,多姿多彩的特性。
第 2 章 使用数据库管理用户权限 | ||
---|---|---|
上一页 | 部分 I. 基础篇 | 下一页 |
上一章节中,我们把用户信息和权限信息放到了xml文件中,这是为了演示如何使用最小的配置就可以使用Spring Security,而实际开发中,用户信息和权限信息通常是被保存在数据库中的,为此Spring Security提供了通过数据库获得用户权限信息的方式。
为了从数据库中获取用户权限信息,我们所需要的仅仅是修改配置文件中的authentication-provider部分。
将上一章配置文件中的user-service替换为jdbc-user-service,替换内容如下所示:
<authentication-provider> <user-service> <user name="admin" password="admin" authorities="ROLE_USER, ROLE_ADMIN" /> <user name="user" password="user" authorities="ROLE_USER" /> </user-service> </authentication-provider>
将上述红色部分替换为下面黄色部分。
<authentication-provider>
<jdbc-user-service data-source-ref="dataSource"/>
</authentication-provider>
现在只要再为jdbc-user-service提供一个dataSource就可以让Spring Security使用数据库中的权限信息了。在此我们使用spring创建一个演示用的dataSource实现,这个dataSource会连接到hsqldb数据库,从中获取用户权限信息。[1]
<beans:bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <beans:property name="driverClassName" value="org.hsqldb.jdbcDriver"/> <beans:property name="url" value="jdbc:hsqldb:res:/hsqldb/test"/> <beans:property name="username" value="sa"/> <beans:property name="password" value=""/> </beans:bean>
最终的配置文件如下所示:
<?xml version="1.0" encoding="UTF-8"?> <beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.4.xsd"> <http auto-config='true'> <intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" /> <intercept-url pattern="/**" access="ROLE_USER" /> </http> <authentication-provider> <jdbc-user-service data-source-ref="dataSource"/> </authentication-provider> <beans:bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <beans:property name="driverClassName" value="org.hsqldb.jdbcDriver"/> <beans:property name="url" value="jdbc:hsqldb:res:/hsqldb/test"/> <beans:property name="username" value="sa"/> <beans:property name="password" value=""/> </beans:bean> </beans:beans>
Spring Security默认情况下需要两张表,用户表和权限表。以下是hsqldb中的建表语句:
create table users(username varchar_ignorecase(50) not null primary key, password varchar_ignorecase(50) not null, enabled boolean not null ); create table authorities (
username varchar_ignorecase(50) not null, authority varchar_ignorecase(50) not null, constraint fk_authorities_users foreign key(username) references users(username) ); create unique index ix_auth_username on authorities (username,authority);
![]()
users:用户表。包含username用户登录名,password登陆密码,enabled用户是否被禁用三个字段。 其中username用户登录名为主键。 | |
authorities:权限表。包含username用户登录名,authorities对应权限两个字段。 其中username字段与users用户表的主键使用外键关联。 | |
对authorities权限表的username和authority创建唯一索引,提高查询效率。 |
Spring Security会在初始化时,从这两张表中获得用户信息和对应权限,将这些信息保存到缓存中。其中users表中的登录名和密码用来控制用户的登录,而权限表中的信息用来控制用户登陆后是否有权限访问受保护的系统资源。
我们在示例中预先初始化了一部分数据:
insert into users(username,password,enabled) values('admin','admin',true); insert into users(username,password,enabled) values('user','user',true); insert into authorities(username,authority) values('admin','ROLE_ADMIN'); insert into authorities(username,authority) values('admin','ROLE_USER'); insert into authorities(username,authority) values('user','ROLE_USER');
上述sql中,我们创建了两个用户admin和user,其中admin拥有ROLE_ADMIN和ROLE_USER权限,而user只拥有ROLE_USER权限。这和我们上一章中的配置相同,因此本章实例的效果也和上一章完全相同,这里就不再赘述了。
实例见ch002。
[1] javax.sql.DataSource是一个用来定义数据库连接池的统一接口。当我们想调用任何实现了javax.sql.DataSource接口的连接池,只需要调用接口提供的getConnection()就可以获得连接池中的jdbc连接。javax.sql.DataSource可以屏蔽连接池的不同实现,我们使用的连接池即可能由第三方包单独提供,也可能是由j2ee容器统一管理提供的。
第 3 章 自定义数据库表结构 | ||
---|---|---|
上一页 | 部分 I. 基础篇 | 下一页 |
Spring Security默认提供的表结构太过简单了,其实就算默认提供的表结构很复杂,也无法满足所有企业内部对用户信息和权限信息管理的要求。基本上每个企业内部都有一套自己的用户信息管理结构,同时也会有一套对应的权限信息体系,如何让Spring Security在这些已有的数据结构之上运行呢?
假设我们实际使用的表结构如下所示:
-- 角色 create table role( id bigint, name varchar(50), descn varchar(200) ); alter table role add constraint pk_role primary key(id); alter table role alter column id bigint generated by default as identity(start with 1); -- 用户 create table user( id bigint, username varchar(50), password varchar(50), status integer, descn varchar(200) ); alter table user add constraint pk_user primary key(id); alter table user alter column id bigint generated by default as identity(start with 1); -- 用户角色连接表 create table user_role( user_id bigint, role_id bigint ); alter table user_role add constraint pk_user_role primary key(user_id, role_id); alter table user_role add constraint fk_user_role_user foreign key(user_id) references user(id); alter table user_role add constraint fk_user_role_role foreign key(role_id) references role(id);
上述共有三张表,其中user用户表,role角色表为保存用户权限数据的主表,user_role为关联表。user用户表,role角色表之间为多对多关系,就是说一个用户可以有多个角色。ER图如下所示:
创建两个用户,admin和user。admin用户拥有“管理员”角色,user用户拥有“用户”角色。
insert into user(id,username,password,status,descn) values(1,'admin','admin',1,'管理员'); insert into user(id,username,password,status,descn) values(2,'user','user',1,'用户'); insert into role(id,name,descn) values(1,'ROLE_ADMIN','管理员角色'); insert into role(id,name,descn) values(2,'ROLE_USER','用户角色'); insert into user_role(user_id,role_id) values(1,1); insert into user_role(user_id,role_id) values(1,2); insert into user_role(user_id,role_id) values(2,2);
现在我们要在这样的数据结构基础上使用Spring Security,Spring Security所需要的数据只是为了处理两种情况,一是判断登录用户是否合法,二是判断登陆的用户是否有权限访问受保护的系统资源。
我们所要做的工作就是在现有数据结构的基础上,为Spring Security提供这两种数据。
当用户登陆时,系统需要判断用户登录名是否存在,登陆密码是否正确,当前用户是否被禁用。我们使用下列SQL来提取这三个信息。
select username,password,status as enabled from user where username=?
用户登陆之后,系统需要获得该用户的所有权限,根据用户已被赋予的权限来判断哪些系统资源可以被用户访问,哪些资源不允许用户访问。
以下SQL就可以获得当前用户所拥有的权限。
select u.username,r.name as authority from user u join user_role ur on u.id=ur.user_id join role r on r.id=ur.role_id where u.username=?"/>
将这两条SQL语句配置到xml中,就可以让Spring Security从我们自定义的表结构中提取数据了。最终配置文件如下所示:
<authentication-provider> <jdbc-user-service data-source-ref="dataSource"users-by-username-query="select username,password,status as enabled from user where username=?"
authorities-by-username-query="select u.username,r.name as authority from user u join user_role ur on u.id=ur.user_id join role r on r.id=ur.role_id where u.username=?"/> </authentication-provider>
users-by-username-query为根据用户名查找用户,系统通过传入的用户名查询当前用户的登录名,密码和是否被禁用这一状态。 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
authorities-by-username-query为根据用户名查找权限,系统通过传入的用户名查询当前用户已被授予的所有权限。
Spring Security虽然默认提供了一个登陆页面,但是这个页面实在太简陋了,只有在快速演示时才有可能它做系统的登陆页面,实际开发时无论是从美观还是实用性角度考虑,我们都必须实现自定义的登录页面。 自己实现一个login.jsp,放在src/main/webapp/目录下。 + ch004/
+ src/
+ main/
+ resources/
* applicationContext.xml
+ webapp/
+ WEB-INF/
* web.xml
* admin.jsp
* index.jsp
* login.jsp
+ test/
+ resources/
* pom.xml
在xml中的http标签中添加一个form-login标签。 <http auto-config='true'> <intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY" />
以下是我们创建的login.jsp页面的主要代码。 <div class="error ${param.error == true ? '' : 'hide'}"> 登陆失败<br> ${sessionScope['SPRING_SECURITY_LAST_EXCEPTION'].message} </div> <form action="${pageContext.request.contextPath}/j_spring_security_check
以上介绍了自定义页面上Spring Security所需的基本元素,这些参数名称都采用了Spring Security中默认的配置值,如果有特殊需要还可以通过配置文件进行修改。 经过以上配置,我们终于使用了一个自己创建的登陆页面替换了原来Spring Security默认提供的登录页面了。我们不仅仅是做个样子,而是实际配置了各个Spring Security所需的参数,真正将自定义登陆页面与Spring Security紧紧的整合在了一起。以下是使用自定义登陆页面实际运行时的截图。
国内对权限系统的基本要求是将用户权限和被保护资源都放在数据库里进行管理,在这点上Spring Security并没有给出官方的解决方案,为此我们需要对Spring Security进行扩展。 这次我们使用五张表,user用户表,role角色表,resc资源表相互独立,它们通过各自之间的连接表实现多对多关系。 -- 资源 create table resc( id bigint, name varchar(50), res_type varchar(50), res_string varchar(200), priority integer, descn varchar(200) ); alter table resc add constraint pk_resc primary key(id); alter table resc alter column id bigint generated by default as identity(start with 1); -- 角色 create table role( id bigint, name varchar(50), descn varchar(200) ); alter table role add constraint pk_role primary key(id); alter table role alter column id bigint generated by default as identity(start with 1); -- 用户 create table user( id bigint, username varchar(50), password varchar(50), status integer, descn varchar(200) ); alter table user add constraint pk_user primary key(id); alter table user alter column id bigint generated by default as identity(start with 1); -- 资源角色连接表 create table resc_role( resc_id bigint, role_id bigint ); alter table resc_role add constraint pk_resc_role primary key(resc_id, role_id); alter table resc_role add constraint fk_resc_role_resc foreign key(resc_id) references resc(id); alter table resc_role add constraint fk_resc_role_role foreign key(role_id) references role(id); -- 用户角色连接表 create table user_role( user_id bigint, role_id bigint ); alter table user_role add constraint pk_user_role primary key(user_id, role_id); alter table user_role add constraint fk_user_role_user foreign key(user_id) references user(id); alter table user_role add constraint fk_user_role_role foreign key(role_id) references role(id); user表中包含用户登陆信息,role角色表中包含授权信息,resc资源表中包含需要保护的资源。 ER图如下所示: 创建的两个用户分别对应“管理员”角色和“用户”角色。而“管理员”角色可以访问“/admin.jsp”和“/**”,“用户”角色只能访问“/**”。 insert into user(id,username,password,status,descn) values(1,'admin','admin',1,'管理员'); insert into user(id,username,password,status,descn) values(2,'user','user',1,'用户'); insert into role(id,name,descn) values(1,'ROLE_ADMIN','管理员角色'); insert into role(id,name,descn) values(2,'ROLE_USER','用户角色'); insert into resc(id,name,res_type,res_string,priority,descn) values(1,'','URL','/admin.jsp',1,''); insert into resc(id,name,res_type,res_string,priority,descn) values(2,'','URL','/**',2,''); insert into resc_role(resc_id,role_id) values(1,1); insert into resc_role(resc_id,role_id) values(2,1); insert into resc_role(resc_id,role_id) values(2,2); insert into user_role(user_id,role_id) values(1,1); insert into user_role(user_id,role_id) values(1,2); insert into user_role(user_id,role_id) values(2,2); Spring Security没有提供从数据库获得获取资源信息的方法,实际上Spring Security甚至没有为我们留一个半个的扩展接口,所以我们这次要费点儿脑筋了。 首先,要搞清楚需要提供何种类型的数据,然后,寻找可以让我们编写的代码替换原有功能的切入点,实现了以上两步之后,就可以宣布大功告成了。 从配置文件上可以看到,Spring Security所需的数据应该是一系列URL网址和访问这些网址所需的权限: <intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY" /> <intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" /> <intercept-url pattern="/**" access="ROLE_USER" /> Spring Security所做的就是在系统初始化时,将以上XML中的信息转换为特定的数据格式,而框架中其他组件可以利用这些特定格式的数据,用于控制之后的验证操作。 现在这些资源信息都保存在数据库中,我们可以使用上面介绍的SQL语句从数据中查询。 select re.res_string,r.name from role r join resc_role rr on r.id=rr.role_id join resc re on re.id=rr.resc_id order by re.priority 下面要开始编写实现代码了。
这样我们就获得了DefaultFilterInvocationDefinitionSource,剩下的只差把这个我们自己创建的类替换掉原有的代码了。 完整代码如下所示: package com.family168.springsecuritybook.ch005; import java.sql.ResultSet; import java.sql.SQLException; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.sql.DataSource; import org.springframework.beans.factory.FactoryBean; import org.springframework.jdbc.core.support.JdbcDaoSupport; import org.springframework.jdbc.object.MappingSqlQuery; import org.springframework.security.ConfigAttributeDefinition; import org.springframework.security.ConfigAttributeEditor; import org.springframework.security.intercept.web.DefaultFilterInvocationDefinitionSource; import org.springframework.security.intercept.web.FilterInvocationDefinitionSource; import org.springframework.security.intercept.web.RequestKey; import org.springframework.security.util.AntUrlPathMatcher; import org.springframework.security.util.UrlMatcher; public class JdbcFilterInvocationDefinitionSourceFactoryBean extends JdbcDaoSupport implements FactoryBean { private String resourceQuery; public boolean isSingleton() { return true; } public Class getObjectType() { return FilterInvocationDefinitionSource.class; } public Object getObject() { return new DefaultFilterInvocationDefinitionSource(this .getUrlMatcher(), this.buildRequestMap()); } protected Map<String, String> findResources() { ResourceMapping resourceMapping = new ResourceMapping(getDataSource(), resourceQuery); Map<String, String> resourceMap = new LinkedHashMap<String, String>(); for (Resource resource : (List<Resource>) resourceMapping.execute()) { String url = resource.getUrl(); String role = resource.getRole(); if (resourceMap.containsKey(url)) { String value = resourceMap.get(url); resourceMap.put(url, value + "," + role); } else { resourceMap.put(url, role); } } return resourceMap; } protected LinkedHashMap<RequestKey, ConfigAttributeDefinition> buildRequestMap() { LinkedHashMap<RequestKey, ConfigAttributeDefinition> requestMap = null; requestMap = new LinkedHashMap<RequestKey, ConfigAttributeDefinition>(); ConfigAttributeEditor editor = new ConfigAttributeEditor(); Map<String, String> resourceMap = this.findResources(); for (Map.Entry<String, String> entry : resourceMap.entrySet()) { RequestKey key = new RequestKey(entry.getKey(), null); editor.setAsText(entry.getValue()); requestMap.put(key, (ConfigAttributeDefinition) editor.getValue()); } return requestMap; } protected UrlMatcher getUrlMatcher() { return new AntUrlPathMatcher(); } public void setResourceQuery(String resourceQuery) { this.resourceQuery = resourceQuery; } private class Resource { private String url; private String role; public Resource(String url, String role) { this.url = url; this.role = role; } public String getUrl() { return url; } public String getRole() { return role; } } private class ResourceMapping extends MappingSqlQuery { protected ResourceMapping(DataSource dataSource, String resourceQuery) { super(dataSource, resourceQuery); compile(); } protected Object mapRow(ResultSet rs, int rownum) throws SQLException { String url = rs.getString(1); String role = rs.getString(2); Resource resource = new Resource(url, role); return resource; } } } 在spring中配置我们编写的代码。 <beans:bean id="filterInvocationDefinitionSource" class="com.family168.springsecuritybook.ch005.JdbcFilterInvocationDefinitionSourceFactoryBean"> <beans:property name="dataSource" ref="dataSource"/> <beans:property name="resourceQuery" value=" select re.res_string,r.name from role r join resc_role rr on r.id=rr.role_id join resc re on re.id=rr.resc_id order by re.priority "/> </beans:bean> 下一步使用这个filterInvocationDefinitionSource创建filterSecurityInterceptor,并使用它替换系统原来创建的那个过滤器。 <beans:bean id="filterSecurityInterceptor"
class="org.springframework.security.intercept.web.FilterSecurityInterceptor" autowire="byType">
<custom-filter before="FILTER_SECURITY_INTERCEPTOR" />
<beans:property name="objectDefinitionSource" ref="filterInvocationDefinitionSource" />
</beans:bean>
注意这个custom-filter标签,它表示将filterSecurityInterceptor放在框架原来的FILTER_SECURITY_INTERCEPTOR过滤器之前,这样我们的过滤器会先于原来的过滤器执行,因为它的功能与老过滤器完全一样,所以这就等于把原来的过滤器替换掉了。 完整的配置文件如下所示: <?xml version="1.0" encoding="UTF-8"?> <beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.4.xsd"> <http auto-config="true"/> <authentication-provider> <jdbc-user-service data-source-ref="dataSource" users-by-username-query="select username,password,status as enabled from user where username=?" authorities-by-username-query="select u.username,r.name as authority from user u join user_role ur on u.id=ur.user_id join role r on r.id=ur.role_id where u.username=?"/> </authentication-provider> <beans:bean id="filterSecurityInterceptor" class="org.springframework.security.intercept.web.FilterSecurityInterceptor" autowire="byType"> <custom-filter before="FILTER_SECURITY_INTERCEPTOR" /> <beans:property name="objectDefinitionSource" ref="filterInvocationDefinitionSource" /> </beans:bean> <beans:bean id="filterInvocationDefinitionSource" class="com.family168.springsecuritybook.ch05.JdbcFilterInvocationDefinitionSourceFactoryBean"> <beans:property name="dataSource" ref="dataSource"/> <beans:property name="resourceQuery" value=" select re.res_string,r.name from role r join resc_role rr on r.id=rr.role_id join resc re on re.id=rr.resc_id order by re.priority "/> </beans:bean> <beans:bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <beans:property name="driverClassName" value="org.hsqldb.jdbcDriver"/> <beans:property name="url" value="jdbc:hsqldb:res:/hsqldb/test"/> <beans:property name="username" value="sa"/> <beans:property name="password" value=""/> </beans:bean> </beans:beans> 实例见ch05。 目前存在的问题是,系统会在初始化时一次将所有资源加载到内存中,即使在数据库中修改了资源信息,系统也不会再次去从数据库中读取资源信息。这就造成了每次修改完数据库后,都需要重启系统才能时资源配置生效。 解决方案是,如果数据库中的资源出现的变化,需要刷新内存中已加载的资源信息时,使用下面代码: <%@page import="org.springframework.context.ApplicationContext"%> <%@page import="org.springframework.web.context.support.WebApplicationContextUtils"%> <%@page import="org.springframework.beans.factory.FactoryBean"%> <%@page import="org.springframework.security.intercept.web.FilterSecurityInterceptor"%> <%@page import="org.springframework.security.intercept.web.FilterInvocationDefinitionSource"%> <% ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(application); FactoryBean factoryBean = (FactoryBean) ctx.getBean("&filterInvocationDefinitionSource"); FilterInvocationDefinitionSource fids = (FilterInvocationDefinitionSource) factoryBean.getObject(); FilterSecurityInterceptor filter = (FilterSecurityInterceptor) ctx.getBean("filterSecurityInterceptor"); filter.setObjectDefinitionSource(fids); %> <jsp:forward page="/"/> 目前还不支持对方法调用和ACL资源的动态管理,相关讨论请参考手册后面的部分第 39 章 动态资源管理。 让我们来研究一些与用户信息相关的功能,包括为用户密码加密,缓存用户信息,获得系统当前登陆的用户,获得登陆用户的所有权限。 任何一个正式的企业应用中,都不会在数据库中使用明文来保存密码的,我们在之前的章节中都是为了方便起见没有对数据库中的用户密码进行加密,这在实际应用中是极为幼稚的做法。可以想象一下,只要有人进入数据库就可以看到所有人的密码,这是一件多么恐怖的事情,为此我们至少要对密码进行加密,这样即使数据库被攻破,也可以保证用户密码的安全。 最常用的方法是使用MD5算法对密码进行摘要加密,这是一种单项加密手段,无法通过加密后的结果反推回原来的密码明文。 首先我们要把数据库中原来保存的密码使用MD5进行加密: INSERT INTO USERS VALUES('admin','21232f297a57a5a743894a0e4a801fc3',TRUE) INSERT INTO USERS VALUES('user','ee11cbb19052e40b07aac0ca060c23ee',TRUE) 现在密码部分已经面目全非了,即使有人攻破了数据库,拿到这种“乱码”也无法登陆系统窃取客户的信息。 下一步为了让Spring Security支持MD5加密,我们需要修改一下配置文件。 <authentication-provider>
<password-encoder hash="md5"/>
<jdbc-user-service data-source-ref="dataSource"/>
</authentication-provider>
上述代码中新增的黄色部分,将启用MD5算法。用户登录时,输入的密码是明文,需要使用password-encoder将明文转换成md5形式,然后再与数据库中的已加密密码进行比对。 这些配置对普通客户不会造成任何影响,他们只需要输入自己的密码,Spring Security会自动加以演算,将生成的结果与数据库中保存的信息进行比对,以此来判断用户是否可以登陆。 这样,我们只添加了一行配置,就为系统带来了密码加密的功能。 实际上,上面的实例在现实使用中还存在着一个不小的问题。虽然md5算法是不可逆的,但是因为它对同一个字符串计算的结果是唯一的,所以一些人可能会使用“字典攻击”的方式来攻破md5加密的系统[5]。这虽然属于暴力解密,却十分有效,因为大多数系统的用户密码都不回很长。 实际上,大多数系统都是用admin作为默认的管理员登陆密码,所以,当我们在数据库中看到“21232f297a57a5a743894a0e4a801fc3”时,就可以意识到admin用户使用的密码了。因此,md5在处理这种常用字符串时,并不怎么奏效。 为了解决这个问题,我们可以使用盐值加密“salt-source”。 修改配置文件: <authentication-provider> <password-encoder hash="md5"> <salt-source user-property="username"/> </password-encoder> <jdbc-user-service data-source-ref="dataSource"/> </authentication-provider> 在password-encoder下添加了salt-source,并且指定使用username作为盐值。 盐值的原理非常简单,就是先把密码和盐值指定的内容合并在一起,再使用md5对合并后的内容进行演算,这样一来,就算密码是一个很常见的字符串,再加上用户名,最后算出来的md5值就没那么容易猜出来了。因为攻击者不知道盐值的值,也很难反算出密码原文。 我们这里将每个用户的username作为盐值,最后数据库中的密码部分就变成了这样: INSERT INTO USERS VALUES('admin','ceb4f32325eda6142bd65215f4c0f371',TRUE) INSERT INTO USERS VALUES('user','47a733d60998c719cf3526ae7d106d13',TRUE) 介于系统的用户信息并不会经常改变,因此使用缓存就成为了提升性能的一个非常好的选择。Spring Security内置的缓存实现是基于ehcache的,为了启用缓存功能,我们要在配置文件中添加相关的内容。 <authentication-provider>
<password-encoder hash="md5">
<salt-source user-property="username"/>
</password-encoder>
<jdbc-user-service data-source-ref="dataSource" cache-ref="userCache"/>
</authentication-provider>
我们在jdbc-user-service部分添加了对userCache的引用,它将使用这个bean作为用户权限缓存的实现。对userCache的配置如下所示: <beans:bean id="userCache" class="org.springframework.security.providers.dao.cache.EhCacheBasedUserCache"> <beans:property name="cache" ref="userEhCache"/> </beans:bean> <beans:bean id="userEhCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean"> <beans:property name="cacheManager" ref="cacheManager"/> <beans:property name="cacheName" value="userCache"/> </beans:bean> <beans:bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"/> EhCacheBasedUserCache是Spring Security内置的缓存实现,它将为jdbc-user-service提供缓存功能。它所引用的userEhCache来自spring提供的EhCacheFactoryBean和EhCacheManagerFactoryBean,对于userCache的缓存配置放在ehcache.xml中: <ehcache> <diskStore path="java.io.tmpdir"/> <defaultCache maxElementsInMemory="1000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" /> <cache name="userCache" maxElementsInMemory="100" 如果想了解有关ehcache的更多配置,可以访问它的官方网站http://ehcache.sf.net/。 这样,我们就为用户权限信息设置好了缓存,当一个用户多次访问应用时,不需要每次去访问数据库了,ehcache会将对应的信息缓存起来,这将极大的提高系统的相应速度,同时也避免数据库符合过高的风险。 注意cache-ref隐藏着一个陷阱,如果不看代码,我们也许会误认为cache-ref会在JdbcUserDetailsManager中设置对应的userCache,然后只要直接执行JdbcUserDetailsManager中的方法,就可以自动维护用户缓存。 可惜,cache-ref实际上是在JdbcUserDetailsManager的基础上,生成了一个CachingUserService,这个CachedUserDetailsService会拦截loadUserByUsername()方法,实现读取用户信息的缓存功能。我们在cache-ref中引用的UserCache实际上是放在CacheUserDetailsService中,而不是放到了原有的JdbcUserDetailsManager中,这就会导致JdbcUserDetailsManager中对用户缓存的操作全部失效。 如果只是想从页面上显示当前登陆的用户名,可以直接使用Spring Security提供的taglib。 <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> <div>username : <sec:authentication property="name"/></div> 如果想在程序中获得当前登陆用户对应的对象。 UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext() .getAuthentication() .getPrincipal(); 如果想获得当前登陆用户所拥有的所有权限。 GrantedAuthority[] authorities = userDetails.getAuthorities(); 关于UserDetails是如何放到SecuirtyContext中去的,以及Spring Security所使用的TheadLocal模式,我们会在后面详细介绍。这里我们已经了解了如何获得当前登陆用户的信息。
如果想将动态管理资源与自定义登录页面一起使用,最简单的办法就是在数据库中将登录页面对应的权限设置为IS_AUTHENTICATED_ANONYMOUSLY。 因此在数据库中添加一条资源信息。 INSERT INTO RESC VALUES(1,'','URL','/login.jsp*',1,'') 这里的/login.jsp*就是我们自定义登录页面的地址。 然后为匿名用户添加一条角色信息: INSERT INTO ROLE VALUES(3,'IS_AUTHENTICATED_ANONYMOUSLY','anonymous') 最后为这两条记录进行关联即可。 INSERT INTO RESC_ROLE VALUES(1,3) 这样就实现了将动态管理资源与自定义登录页面进行结合。 提示因为Spring Security对方法调用和ACL权限控制的实现都是基于Spring的AOP,所以只能保护定义在applicationContext.xml中的Java类,对直接new出来的对象是无法保护的。 这里有三种方式可以选择: 使用global-method-security和protect-point标签来管理全局范围的方法权限。 为了在spring中使用AOP,我们要为项目添加几个依赖库。 <dependency> <groupId>cglib</groupId> <artifactId>cglib-nodep</artifactId> <version>2.1_3</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.6.5</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.6.5</version> </dependency> 首先来看看我们将要保护的java类。 package com.family168.springsecuritybook.ch201; public class MessageServiceImpl implements MessageService { public String adminMessage() { return "admin message"; } public String adminDate() { return "admin " + System.currentTimeMillis(); } public String userMessage() { return "user message"; } public String userDate() { return "user " + System.currentTimeMillis(); } } 这里使用的是spring-2.0中的aop语法,对MessageService中所有以admin开头的方法进行权限控制,限制这些方法只能由ROLE_ADMIN调用。 <global-method-security> <protect-pointcut expression="execution(* com.family168.springsecuritybook.ch201.MesageServiceImpl.admin*(..))" access="ROLE_ADMIN"/> </global-method-security> 现在只有拥有ROLE_ADMIN权限的用户才能调用MessageService中以admin开头的方法了,当我们以user/user登陆系统时,尝试调用MessageService类的adminMessage()会跑出一个“访问被拒绝”的异常。 在bean中嵌入intercept-methods和protect标签。 这需要改造配置文件。 <beans:bean id="messageService" class="com.family168.springsecuritybook.ch201.MessageServiceImpl"> <intercept-methods> <protect access="ROLE_ADMIN" method="userMessage"/> </intercept-methods> </beans:bean> 现在messageService中的userMessage()方法只允许拥有ROLE_ADMIN权限的用户才能调用了。 使用intercept-methods面临着几个问题首先,intercept-methods只能使用jdk14的方式拦截实现了接口的类,而不能用cglib直接拦截无接口的类。 其次,intercept-methods和global-method-security一起使用,同时使用时,global-method-security一切正常,intercept-methods则会完全不起作用。 借助jdk5以后支持的annotation,我们直接在代码中设置某一方法的调用权限。 现在有两种选择,使用Spring Security提供的Secured注解,或者使用jsr250规范中定义的注解。 首先修改global-method-security中的配置,添加支持annotation的参数。 <global-method-security secured-annotations="enabled"/>
然后添加依赖包。 <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-core-tiger</artifactId> <version>2.0.5.RELEASE</version> </dependency> 现在我们随便在java代码中添加注解了。 package com.family168.springsecuritybook.ch201;
import org.springframework.security.annotation.Secured;
public class MessageServiceImpl implements MessageService {
@Secured({"ROLE_ADMIN", "ROLE_USER"})
public String userMessage() {
return "user message";
}
}
在Secured中设置了ROLE_ADMIN和ROLE_USER两个权限,只要当前用户拥有其中任意一个权限都可以调用这个方法。 首先还是要修改配置文件。 <global-method-security secured-annotations="enabled"
jsr250-annotations="enabled"/>
然后添加依赖包。 <dependency> <groupId>javax.annotation</groupId> <artifactId>jsr250-api</artifactId> <version>1.0</version> </dependency> 现在可以在代码中使用jsr250中的注解了。 package com.family168.springsecuritybook.ch201; import javax.annotation.security.DenyAll; import javax.annotation.security.PermitAll; import javax.annotation.security.RolesAllowed; public class MessageServiceImpl implements MessageService { @RolesAllowed({"ROLE_ADMIN", "ROLE_USER"}) 从实际使用上来讲,jsr250里多出来的DenyAll和PermitAll纯属浪费,谁会定义谁也不能调用的方法呢?实际上,要是annotation支持布尔操作就好了,比如逻辑并,逻辑或,逻辑否之类的。 还有jsr250中未被支持的RunAs注解,如果能利用起来估计更有趣。 实例在ch201。 TODO: 需要演示一下如何在数据库中配置方法权限控制。
Spring Security作为权限管理框架,其内部机制可分为两大部分,其一是认证授权auhorization,其二是权限校验authentication。 认证授权authorization是指,根据用户提供的身份凭证,生成权限实体,并为之授予相应的权限。 权限校验authentication是指,用户请求访问被保护资源时,将被保护资源所需的权限和用户权限实体所拥护的权限二者进行比对,如果校验通过则用户可以访问被保护资源,否则拒绝访问。 我们之前讲过的form-login,http-basic, digest都属于认证授权authorization部分的概念,用户可以通过这些机制登录到系统中,系统会为用户生成权限主体,并授予相应的权限。 与之相对的,FilterSecurityInterceptor,Method保护,taglib,@Secured都属于权限校验authentication,无论是对URL的请求,对方法的调用,页面信息的显示,都要求用户拥有相应的权限才能访问,否则请求即被拒绝。 为使所有的组件都可以通过同一方式访问当前的权限实体,Spring Security特别提供了SecurityContext作为安全上下文,可以直接通过SecurityContextHolder获得当前线程中的SecurityContext。 SecurityContext securityContext = SecurityContextHolder.getContext(); 默认情况下,SecurityContext的实现基于ThreadLocal,系统会在每次用户请求时将SecurityContext与当前Thread进行绑定,这在web系统中是很常用的使用方式,服务器维护的线程池允许多个用户同时并发访问系统,而ThreadLocal可以保证隔离不同Thread之间的信息。 当时对于单机应用来说,因为只有一个人使用,并不存在并发的情况,所以完全可以让所有Thread都共享同一个SecurityContext,因此Spring Security为我们提供了不同的策略模式,我们可以通过设置系统变量的方式选择希望使用的策略类。 java -Dspring.security.strategy=MODE_GLOBAL com.family168.springsecuritybook.Main 也可以调用SecurityContextHolder的setStrategyName()方法来修改系统使用的策略。 SecurityContextHolder.setStrategyName("MODE_GLOBAL"); SecurityContext中保存着实现了Authentication接口的对象,如果用户尚未通过认证,那么SecurityContext.getAuthenticaiton()方法就会返回null。 可以使用Authentication接口中定义的几个方法,获得当前权限实体的信息。 public interface Authentication extends Principal, Serializable { GrantedAuthority[] getAuthorities(); 默认情况下,会在某一个进行认证的过滤器中生成一个UsernamePasswordAuthenticationToken实例,并将此实例放到SecurityContext中。
Spring Security中的UserDetails被作为一个通用的权限主体,凡是涉及到username和password的情况,都会使用到UserDetails和它对应的服务。 常用的服务有从内存中读取用户信息的InMemoryDaoImpl和用数据库中读取用户信息的JdbcDaoImp。它们都实现了UserDetailsService,因此都可以使用loadUserByUsername()方法获得对应用户的信息。 如果使用了LDAP,还会接触到LdapUserDetailsService,它用来从LDAP中获取用户信息。 在org.springframework.security.userdetails包下还包含一个check目录,它主要用来校验用户是否过期,是否被锁定,是否被禁用。 还可以看到一个hierarchicalroles,它的作用是处理角色继承关系,如果希望使用角色继承策略,需要将原始的UserDetailsService通过UserDetailsServiceWrapper进行一下封装,从而获得由UserDetailsWrapper封装的UserDetails,以此来实现角色继承机制。 在Spring Security中,我们可以指定角色间的继承关系,这样可以重用角色权限,减少配置的代码量,让权限配置整体上显得更清晰。 为了使用角色继承功能,我们需要对原有的配置文件进行一些修改。 <authentication-provider user-service-ref="userDetailsServiceWrapper"/> <beans:bean id="userDetailsServiceWrapper" class="org.springframework.security.userdetails.hierarchicalroles.UserDetailsServiceWrapper"> <beans:property name="userDetailsService" ref="userDetailsService"/> <beans:property name="roleHierarchy"> <beans:bean class="org.springframework.security.userdetails.hierarchicalroles.RoleHierarchyImpl"> <beans:property name="hierarchy" value="ROLE_ADMIN > ROLE_USER"/> </beans:bean> </beans:property> </beans:bean> <user-service id="userDetailsService"> <user name="admin" password="admin" authorities="ROLE_ADMIN" /> <user name="user" password="user" authorities="ROLE_USER" /> </user-service> 我们将原有的user-service单独抽离出来,在userDetailsService的基础上生成一个userDetailsServiceWrapper,这个wrapper的作用就是在原有的user-service的基础上启用角色继承功能。 我们使用RoleHierarchyImpl为userDetailsServiceWrapper配置了角色继承的策略,ROLE_ADMIN > ROLE_USER表示ROLE_ADMIN将继承ROLE_USER所有用的所有角色,只要是允许ROLE_USER访问的资源,ROLE_ADMIN也都有权限进行访问。这样我们在user-service中的配置就可以从ROLE_ADMIN,ROLE_USER简化为ROLE_ADMIN了,而intercept-url中的配置也可以从ROLE_ADMIN,ROLE_USER改为ROLE_USER了。 如果希望配置更多继承关系,可以使用换行进行分隔,比如: <property name="hierarchy"> <value> ROLE_A > ROLE_B ROLE_B > ROLE_AUTHENTICATED ROLE_AUTHENTICATED > ROLE_UNAUTHENTICATED </value> </property> 实例在ch205。 目前,直至Spring Security-3.0.0.M1都不支持在acl中使用RoleHierarchy,不过在官网的jira上有人提交了一个patch,如果情况顺利的话,这个patch应该在Spring Security-3.0.0.RC1中被应用到svn中,我们就可以为acl实现角色继承了。 http://jira.springframework.org/browse/SEC-1049。 如果希望在Spring Security-2.x中在acl部分实现角色继承,需要进行如下配置。 首先根据jira上的patch自己创建一个SidRoleHierarchyRetrievalStrategyImpl.java。 /* Copyright 2008 Thomas Champagne * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.security.acls.sid; import java.util.List; import org.springframework.security.Authentication; import org.springframework.security.GrantedAuthority; import org.springframework.security.userdetails.hierarchicalroles.RoleHierarchy; import org.springframework.util.Assert; /** * Extended SidRetrievalStrategyImpl which uses a {@link RoleHierarchy} definition to determine the * roles allocated to the current user. * @author Thomas Champagne */ public class SidRoleHierarchyRetrievalStrategyImpl extends SidRetrievalStrategyImpl { private RoleHierarchy roleHierarchy = null; public SidRoleHierarchyRetrievalStrategyImpl(RoleHierarchy roleHierarchy) { Assert.notNull(roleHierarchy, "RoleHierarchy must not be null"); this.roleHierarchy = roleHierarchy; } /** * Calls the <tt>RoleHierarchy</tt> to obtain the complete set of user authorities. */ GrantedAuthority[] extractAuthorities(Authentication authentication) { return roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities()); } public Sid[] getSids(Authentication authentication) { GrantedAuthority[] authorities = this.extractAuthorities(authentication); Sid[] sids = new Sid[authorities.length + 1]; sids[0] = new PrincipalSid(authentication); for (int i = 1; i <= authorities.length; i++) { sids[i] = new GrantedAuthoritySid(authorities[i - 1]); } return sids; } } 然后在acl的配置文件中配置bean,并在AclEntryVoter,AclEntryAfterInvocationProvider和AclEntryAfterInvocationCollectionFilteringProvider中替换默认的SidRetrievalStrategy。 <bean id="sidRetrievalStrategy" class="org.springframework.security.acls.sid.SidRoleHierarchyRetrievalStrategyImpl"> <constructor-arg ref="roleHierarchy"/> </bean> <bean id="afterAclRead" class="org.springframework.security.afterinvocation.AclEntryAfterInvocationProvider"> <sec:custom-after-invocation-provider/> <constructor-arg ref="aclService"/> <constructor-arg> <list> <util:constant static-field="org.springframework.security.acls.domain.BasePermission.ADMINISTRATION"/> <util:constant static-field="org.springframework.security.acls.domain.BasePermission.READ"/> </list> </constructor-arg> <property name="sidRetrievalStrategy" ref="sidRetrievalStrategy"/> </bean> <bean id="afterAclCollectionRead" class="org.springframework.security.afterinvocation.AclEntryAfterInvocationCollectionFilteringProvider"> <sec:custom-after-invocation-provider/> <constructor-arg ref="aclService"/> <constructor-arg> <list> <util:constant static-field="org.springframework.security.acls.domain.BasePermission.ADMINISTRATION"/> <util:constant static-field="org.springframework.security.acls.domain.BasePermission.READ"/> </list> </constructor-arg> <property name="sidRetrievalStrategy" ref="sidRetrievalStrategy"/> </bean> 这样就在acl中添加了对角色继承的支持。 默认提供的PasswordEncoder包含plaintext, sha, sha-256, md5, md4, {sha}, {ssha}。其中{sha}和{ssha}是专门为ldap准备的,plaintext意味着不对密码进行加密,如果我们不设置PasswordEncoder,默认就会使用它。 SaltValue是为了让密码加密更加安全,它有两种策略可以选择。user-property, system-wide分别对应着ReflectionSaltSource和SystemWideSaltSource,它们的区别是ReflectionSaltSource会使用反射,从用户的Principal对象汇总取出一个对应的属性来作为盐值,而SystemWideSaltSource会为所有用户都设置相同的盐值。 使用了PasswordEncoder和SaltValue的结果就是数据库中的密码变得难以辨认了,这就要注意在添加用户时要使用相同的策略对密码进行加密,这才能保证新用户可以正常登陆。
之前在第 5 章 使用数据库管理资源中,我们简要讨论过使用数据库管理资源,为了使手册开始的部分保持简洁,我们没有再深入讨论这个话题,包括实例中存在的一些问题也都没有解决,这一章中,我们会尝试进行更深层次的讨论。 对应的数据库结构与ER图,可以参考第 5 章 使用数据库管理资源。 拦截器与所需的权限配置数据格式,可以参考第 36 章 拦截器。 所有,我们需要做的就是把数据库中的数据读取出来,组装成拦截器所需的格式,然后把权限配置数据放到拦截器里。 为了区分URL和METHOD,我们在resc表中使用res_type字段来区分这两种不同的被保护资源。 res_type="URL"对应将在FilterSecurityInterceptor中使用的被保护网址。 INSERT INTO RESC VALUES(1,'','URL','/admin.jsp',1,'') 这里将/admin.jsp作为一个网址进行保护,随后它将被设置到FilterSecurityInterceptor中。 res_type="METHOD"对应将在MethodSecurityInterceptor中使用的被保护方法。 INSERT INFO RESC VALUES(3,'','METHOD','com.family168.springsecuritybook.ch207.MessageService.adminMessage',3,''); 这里将com.family168.springsecuriytbook.ch207.Message的adminMessage()方法设置为被保护资源,随后它将被设置到MethodSecurityInterceptor中。 我们使用如下sql语句从数据库中分别读取被保护的url和method信息。 读取被保护url信息。 select re.res_string,r.name from role r join resc_role rr on r.id=rr.role_id join resc re on re.id=rr.resc_id where re.res_type='URL' order by re.priority 读取被保护method信息。 select re.res_string,r.name from role r join resc_role rr on r.id=rr.role_id join resc re on re.id=rr.resc_id where re.res_type='METHOD' order by re.priority 为了实现资源的统一配置,我们创建了名为ResourceDetailsMonitor的类用来管理数据库中的被保护资源信息,它负责从数据库中读取原始信息,并转换成FilterSecurityInterceptor和MethodInterceptor所需的数据格式。 为了动态设置FilterSecurityInterceptor中的资源配置,ResouceDetailsMonitor中直接将组装后的FilterInvocationDefinitionSource使用setObjectDefinitionSource()方法设置到FilterSecurityInterceptor中。 FilterInvocationDefinitionSource source = resourceDetailsBuilder .createUrlSource(queryUrl, getUrlMatcher()); filterSecurityInterceptor.setObjectDefinitionSource(source); 之后,FilterSecurityInterceptor就会根据我们设置的资源信息控制用户可以访问哪些资源。 MethodSecurityInterceptor的情况有些复杂,因为涉及到spring中aop的pointcut部分特性,所以直接为MethodSecurityInterceptor设置objectDefinitionSource是不会起作用的。 我们需要获取delegatingMethodDefinitionSource,将数据库中读取的资源信息设置到它里面才能使MethodSecurityInterceptor和动态生成的pointcut都是用我们最新的资源信息。 MethodDefinitionSource source = resourceDetailsBuilder .createMethodSource(queryMethod); List<MethodDefinitionSource> sources = new ArrayList<MethodDefinitionSource>(); sources.add(source); delegatingMethodDefinitionSource.setMethodDefinitionSources(sources); 因为ACL实际上也是借助于MethodSecurityInterceptor来实现的,所以可以将ACL_READ和AFTER_ACL_READ配置在res_type="METHOD"的资源中。 实例在ch207中。 如果希望扩展登录时加载的用户信息,最简单直接的办法就是实现UserDetails接口,定义一个包含所有业务数据的对象。我们下面演示如何将用户邮箱加入UserDetails中。 UserDetails接口中总共声明了六个方法: public interface UserDetails extends Serializable { GrantedAuthority[] getAuthorities(); 我们的任务就是实现这六个接口,同时添加一个getEmail()方法,用以获得用户的邮箱地址。 最初我们的打算是直接继承Spring Security中默认提供的实现类User,但是User为了避免用户信息被外部程序篡改,被设计为只能通过构造方法来为内部数据赋值,没有提供setter方法对其中数据进行修改,因此为了之后演示的方便,我们仿照User类自行实现了一个BaseUserDetails类,在BaseUserDetails中所有属性都被定义为protected,可以暴露给子类进行操作。 在BaseUserDetails的基础上,我们实现了UserInfo类,在它里面添加有关email的属性和方法。 package com.family168.springsecuritybook.ch208; import org.springframework.security.GrantedAuthority; public class UserInfo extends BaseUserDetails { private static final long serialVersionUID = 1L; private String email; public UserInfo(String username, String password, boolean enabled, GrantedAuthority[] authorities) throws IllegalArgumentException { super(username, password, enabled, authorities); } public String getEmail() { return this.email; } public void setEmail(String email) { this.email = email; } } 为了将UserInfo提供给权限系统,我们还需要实现自定义的UserDetailsService,这个接口只包含一个方法: public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException; } 实际运行中,系统会通过这个方法获得登录用户的信息。 下面我们直接实现UserDetailsService接口,在其中创建UserInfo的对象。 public class UserInfoService implements UserDetailsService { private Map<String, UserInfo> userMap = null; public UserInfoService() { userMap = new HashMap<String, UserInfo>(); UserInfo userInfo = null; userInfo = new UserInfo("user", "user", true, new GrantedAuthority[]{ new GrantedAuthorityImpl("ROLE_USER") }); userInfo.setEmail("user@family168.com"); userMap.put("user", userInfo); userInfo = new UserInfo("admin", "admin", true, new GrantedAuthority[]{ new GrantedAuthorityImpl("ROLE_ADMIN"), new GrantedAuthorityImpl("ROLE_USER") }); userInfo.setEmail("admin@family168.com"); userMap.put("admin", userInfo); } public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { return userMap.get(username); } } 将UserInfoService添加到配置文件中: <authentication-provider user-service-ref="userDetailsService"/> <beans:bean id="userDetailsService" class="com.family168.springsecuritybook.ch208.UserInfoService"/> 定义userDetailsService之后,然后使用user-service-ref为authentication-provider设置对UserDetailsService的引用,这样在系统中就会从我们自定义的UserInfoService中获取用户信息了。 修改过配置文件后,在ch208中启动mvn,还是通过登录页面进入系统,在登录成功页面中就可以看到用户对应的邮箱地址了。 这时保存在SecurityContext中的Principal已经变为了UserInfo类型的对象,我们可以直接使用taglib获得启动的邮件信息。 email : <sec:authentication property="principal.email"/> 如果希望获得UserInfo对象,可以使用如下代码: UserInfo userInfo = (UserInfo) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 实例在ch208中。 实例见ch004。 [2] 有关匿名用户的知识,我们会在之后的章节中进行讲解。 [3] 登陆成功后跳转策略的知识,我们会在之后的章节中进行讲解。 [4] 关于绝对路径和相对路径的详细讨论,请参考http://www.family168.com/tutorial/jsp/html/jsp-ch-03.html#jsp-ch-03-04-01
|