信息: 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.
对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');
Spring Security默认提供的表结构太过简单了,其实就算默认提供的表结构很复杂,也无法满足所有企业内部对用户信息和权限信息管理的要求。基本上每个企业内部都有一套自己的用户信息管理结构,同时也会有一套对应的权限信息体系,如何让Spring Security在这些已有的数据结构之上运行呢?
3.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);
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);
<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>
这次我们使用五张表,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);
创建的两个用户分别对应“管理员”角色和“用户”角色。而“管理员”角色可以访问“/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);
5.3. 实现从数据库中读取资源信息
Spring Security没有提供从数据库获得获取资源信息的方法,实际上Spring Security甚至没有为我们留一个半个的扩展接口,所以我们这次要费点儿脑筋了。
首先,要搞清楚需要提供何种类型的数据,然后,寻找可以让我们编写的代码替换原有功能的切入点,实现了以上两步之后,就可以宣布大功告成了。
5.3.1. 需要何种数据格式
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
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;
}
}
}
5.3.2. 替换原有功能的切入点
在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>
<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="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>
目前,直至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.
*/
/**
* 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 = new GrantedAuthoritySid(authorities[i - 1]);
}
为了区分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
实际运行中,系统会通过这个方法获得登录用户的信息。
下面我们直接实现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);
}