國內對權限系統的基本要求是將用戶權限和被保護資源都放在數據庫里進行管理,在這點上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);
ER圖如下表示

圖 5.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 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甚至沒有為我們留一個半個的擴展接口,所以我們這次要費點兒腦筋了。
首先,要搞清楚需要提供何種類型的數據,然后,尋找可以讓我們編寫的代碼替換原有功能的切入點,實現了以上兩步之后,就可以宣布大功告成了。
1.
從配置文件上可以看到,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" />
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
完整代碼
package com.family168.springsecuritybook.ch005; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Collection; 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.access.ConfigAttribute; import org.springframework.security.access.ConfigAttributeEditor; import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.security.web.access.intercept.RequestKey; import org.springframework.security.web.util.AntPathRequestMatcher; import org.springframework.security.web.util.RequestMatcher; public class JdbcFilterInvocationDefinitionSourceFactoryBean extends JdbcDaoSupport implements FactoryBean { private String resourceQuery; public boolean isSingleton() { return true; } public Class getObjectType() { return FilterInvocationSecurityMetadataSource.class; } public Object getObject() { return new DefaultFilterInvocationSecurityMetadataSource(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<RequestMatcher, Collection<ConfigAttribute>> buildRequestMap() { LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap = null; requestMap = new LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>>(); ConfigAttributeEditor editor = new ConfigAttributeEditor(); Map<String, String> resourceMap = this.findResources(); for (Map.Entry<String, String> entry : resourceMap.entrySet()) { String key = entry.getKey(); editor.setAsText(entry.getValue()); requestMap.put(new AntPathRequestMatcher(key), (Collection<ConfigAttribute>) editor.getValue()); } return requestMap; } 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; } } }
完整的配置文件為
<?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.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"> <http auto-config="true"> <custom-filter ref="filterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR" /> </http> <authentication-manager> <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> </authentication-manager> <beans:bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor" autowire="byType"> <beans:property name="securityMetadataSource" ref="filterInvocationSecurityMetadataSource" /> <beans:property name="authenticationManager" ref="org.springframework.security.authenticationManager"/> </beans:bean> <beans:bean id="filterInvocationSecurityMetadataSource" 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> <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>
目前存在的問題是,系統會在初始化時一次將所有資源加載到內存中,即使在數據庫中修改了資源信息,系統也不會再次去從數據庫中讀取資源信息。這就造成了每次修改完數據庫后,都需要重啟系統才能時資源配置生效。
解決方案是,如果數據庫中的資源出現的變化,需要刷新內存中已加載的資源信息時,使用下面代碼:
<% ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(application); FactoryBean factoryBean = (FactoryBean) ctx.getBean("&filterInvocationSecurityMetadataSource"); FilterInvocationSecurityMetadataSource fids = (FilterInvocationSecurityMetadataSource) factoryBean.getObject(); FilterSecurityInterceptor filter = (FilterSecurityInterceptor) ctx.getBean("filterSecurityInterceptor"); filter.setSecurityMetadataSource(fids); %>