在以前的幾篇security教程中,資源和所對應的權限都是在xml中進行配置的,也就在http標籤中配置intercept-url,試想要是配置的對象很少,那還好,可是日常實際開發中都每每是很是多的資源和權限對應,並且寫在配置文件裏面寫改起來還得該源碼配置文件,這顯然是很差的。所以接下來,將用數據庫管理資源和權限的對應關係。數據庫仍是接着以前的,用mysql數據庫,所以也不用另外引入額外的jar包。java
DROP TABLE IF EXISTS `resc`; CREATE TABLE `resc` ( `id` int(20) NOT NULL DEFAULT '0', `name` varchar(50) DEFAULT NULL, `res_type` varchar(50) DEFAULT NULL, `res_string` varchar(200) DEFAULT NULL, `descn` varchar(200) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of resc -- ---------------------------- INSERT INTO `resc` VALUES (1, '', 'URL', '/page/admin.jsp', '管理員頁面'); INSERT INTO `resc` VALUES (2, '', 'URL', '/page/user.jsp', '用戶頁面'); INSERT INTO `resc` VALUES (3, null, 'URL', '/page/test.jsp', '測試頁面'); -- ---------------------------- -- Table structure for resc_role -- ---------------------------- DROP TABLE IF EXISTS `resc_role`; CREATE TABLE `resc_role` ( `resc_id` int(20) NOT NULL DEFAULT '0', `role_id` int(20) NOT NULL DEFAULT '0', PRIMARY KEY (`resc_id`,`role_id`), KEY `fk_resc_role_role` (`role_id`), CONSTRAINT `fk_resc_role_role` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`), CONSTRAINT `fk_resc_role_resc` FOREIGN KEY (`resc_id`) REFERENCES `resc` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of resc_role -- ---------------------------- INSERT INTO `resc_role` VALUES (1, 1); INSERT INTO `resc_role` VALUES (2, 1); INSERT INTO `resc_role` VALUES (2, 2); INSERT INTO `resc_role` VALUES (3, 3); -- ---------------------------- -- Table structure for role -- ---------------------------- DROP TABLE IF EXISTS `role`; CREATE TABLE `role` ( `id` int(20) NOT NULL DEFAULT '0', `name` varchar(50) DEFAULT NULL, `descn` varchar(200) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of role -- ---------------------------- INSERT INTO `role` VALUES (1, 'ROLE_ADMIN', '管理員角色'); INSERT INTO `role` VALUES (2, 'ROLE_USER', '用戶角色'); INSERT INTO `role` VALUES (3, 'ROLE_TEST', '測試角色'); -- ---------------------------- -- Table structure for t_c3p0 -- ---------------------------- DROP TABLE IF EXISTS `t_c3p0`; CREATE TABLE `t_c3p0` ( `a` char(1) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of t_c3p0 -- ---------------------------- -- ---------------------------- -- Table structure for user -- ---------------------------- DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int(20) NOT NULL DEFAULT '0', `username` varchar(50) DEFAULT NULL, `password` varchar(50) DEFAULT NULL, `status` int(11) DEFAULT NULL, `descn` varchar(200) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of user -- ---------------------------- INSERT INTO `user` VALUES (1, 'admin', '123', 1, '管理員'); INSERT INTO `user` VALUES (2, 'user', '123', 1, '用戶'); INSERT INTO `user` VALUES (3, 'test', '123', 1, '測試'); -- ---------------------------- -- Table structure for user_role -- ---------------------------- DROP TABLE IF EXISTS `user_role`; CREATE TABLE `user_role` ( `user_id` int(20) NOT NULL DEFAULT '0', `role_id` int(20) NOT NULL DEFAULT '0', PRIMARY KEY (`user_id`,`role_id`), KEY `fk_user_role_role` (`role_id`), CONSTRAINT `fk_user_role_role` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`), CONSTRAINT `fk_user_role_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of user_role -- ---------------------------- INSERT INTO `user_role` VALUES (1, 1); INSERT INTO `user_role` VALUES (1, 2); INSERT INTO `user_role` VALUES (2, 2); INSERT INTO `user_role` VALUES (3, 3);
Spring Security須要的數據無非就是pattern和access相似鍵值對的數據,就像配置文件中寫的那樣:mysql
<intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY" />1 <intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" /> <intercept-url pattern="/**" access="ROLE_USER" />
其實當項目啓動時,Spring Security所作的就是在系統初始化時,將以上XML中的信息轉換爲特定的數據格式,而框架中其餘組件能夠利用這些特定格式的數據,用於控制以後的驗證操做。如今咱們將這些信息存儲在數據庫中,所以就要想辦法從數據庫中查詢這些數據,因此根據security數據的須要,只須要以下sql語句就能夠:web
select re.res_string,r.name from role r,resc re,resc_role rr where r.id=rr.role_id and re.id=rr.resc_id
在數據中執行這條語句作測試,獲得以下結果:spring
這樣的格式正是security所須要的數據。sql
package com.sunny.auth; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import javax.sql.DataSource; import org.springframework.jdbc.core.support.JdbcDaoSupport; import org.springframework.jdbc.object.MappingSqlQuery; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.web.util.AntPathRequestMatcher; import org.springframework.security.web.util.RequestMatcher; public class JdbcRequestMapBulider extends JdbcDaoSupport{ //查詢資源和權限關係的sql語句 private String resourceQuery = ""; //查詢資源 @SuppressWarnings("unchecked") public List<Resource> findResources() { ResourceMapping resourceMapping = new ResourceMapping(getDataSource(), resourceQuery); return resourceMapping.execute(); } //拼接RequestMap public LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> buildRequestMap() { LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap = new LinkedHashMap<>(); List<Resource> resourceList = this.findResources(); for (Resource resource : resourceList) { RequestMatcher requestMatcher = this.getRequestMatcher(resource.getUrl()); List<ConfigAttribute> list = new ArrayList<ConfigAttribute>(); list.add(new SecurityConfig(resource.getRole())); requestMap.put(requestMatcher, list); } return requestMap; } //經過一個字符串地址構建一個AntPathRequestMatcher對象 protected RequestMatcher getRequestMatcher(String url) { return new AntPathRequestMatcher(url); } public String getResourceQuery() { return resourceQuery; } public void setResourceQuery(String resourceQuery) { this.resourceQuery = resourceQuery; } /** * 內部類,用於封裝訪問地址和權限 * @ClassName: Resource * @Description: TODO(這裏用一句話描述這個類的做用) * @author Sunny * @date 2018年7月4日 下午2:42:59 * */ 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; } } @SuppressWarnings("rawtypes") 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; } } }
說明:數據庫
在將這部以前,先得了解大概下security的運行過程,security實現控制的功能其實就是經過一系列的攔截器來實現的,當用戶登錄的時候,會被AuthenticationProcessingFilter攔截,調用AuthenticationManager的實現類,同時AuthenticationManager會調用ProviderManager來獲取用戶驗證信息,其中不一樣的Provider調用的服務不一樣,由於這些信息能夠是在數據庫上,能夠是在LDAP(輕量目錄訪問協議)服務器上,能夠是xml配置文件上等,這個例子中是數據庫;若是驗證經過後會將用戶的權限信息放到spring的全局緩存SecurityContextHolder中,以備後面訪問資源時使用。當訪問資源,訪問url時,會經過AbstractSecurityInterceptor攔截器攔截,其中會調用FilterInvocationSecurityMetadataSource的方法來獲取被攔截url所需的所有權限,其中FilterInvocationSecurityMetadataSource的經常使用的實現類爲DefaultFilterInvocationSecurityMetadataSource,這個類中有個很關鍵的東西就是requestMap,也就是咱們上面所獲得的數據,在調用受權管理器AccessDecisionManager,這個受權管理器會經過spring的全局緩存SecurityContextHolder獲取用戶的權限信息,還會獲取被攔截的url和被攔截url所需的所有權限,而後根據所配的策略,若是權限足夠,則返回,權限不夠則報錯並調用權限不足頁面。api
根據源碼debug跟蹤得出,其實資源權限關係就放在DefaultFilterInvocationSecurityMetadataSource的requestMap,中的,這個requestMap就是咱們JdbcRequestMapBulider.buildRequestMap()方法所須要的數據類型,所以就想到了咱們自定義一個類繼承FilterInvocationSecurityMetadataSource接口,將數據查出的數據放到requestMap中去。制定類MyFilterInvocationSecurityMetadataSource繼承FilterInvocationSecurityMetadataSource和InitializingBean接口。緩存
package com.sunny.auth; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.InitializingBean; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.security.web.util.RequestMatcher; public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource, InitializingBean{ private final static List<ConfigAttribute> NULL_CONFIG_ATTRIBUTE = null; // 資源權限集合 private Map<RequestMatcher, Collection<ConfigAttribute>> requestMap; //查找數據庫權限和資源關係 private JdbcRequestMapBulider builder; /* * 更具訪問資源的地址查找所須要的權限 */ @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { final HttpServletRequest request = ((FilterInvocation) object).getRequest(); Collection<ConfigAttribute> attrs = NULL_CONFIG_ATTRIBUTE; for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) { if (entry.getKey().matches(request)) { attrs = entry.getValue(); break; } } return attrs; } /* * 獲取全部的權限 */ @Override public Collection<ConfigAttribute> getAllConfigAttributes() { Set<ConfigAttribute> allAttributes = new HashSet<ConfigAttribute>(); for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) { allAttributes.addAll(entry.getValue()); } System.out.println("擁有權限:"+allAttributes.toString()); return allAttributes; } @Override public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); } //綁定requestMap protected Map<RequestMatcher, Collection<ConfigAttribute>> bindRequestMap() { return builder.buildRequestMap(); } @Override public void afterPropertiesSet() throws Exception { this.requestMap = this.bindRequestMap(); } public void refreshResuorceMap() { this.requestMap = this.bindRequestMap(); } /******************* GET & SET *******************************/ public JdbcRequestMapBulider getBuilder() { return builder; } public void setBuilder(JdbcRequestMapBulider builder) { this.builder = builder; } }
說明:服務器
spring-dataSource.xml保持不變session
<?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-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"> <!-- 數據源 --> <beans:bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> <!-- 此爲c3p0在spring中直接配置datasource c3p0是一個開源的JDBC鏈接池 --> <beans:property name="driverClass" value="com.mysql.jdbc.Driver" /> <beans:property name="jdbcUrl" value="jdbc:mysql://localhost:3306/springsecurity?useUnicode=true&characterEncoding=UTF-8" /> <beans:property name="user" value="root" /> <beans:property name="password" value="" /> <beans:property name="maxPoolSize" value="50"></beans:property> <beans:property name="minPoolSize" value="10"></beans:property> <beans:property name="initialPoolSize" value="10"></beans:property> <beans:property name="maxIdleTime" value="25000"></beans:property> <beans:property name="acquireIncrement" value="1"></beans:property> <beans:property name="acquireRetryAttempts" value="30"></beans:property> <beans:property name="acquireRetryDelay" value="1000"></beans:property> <beans:property name="testConnectionOnCheckin" value="true"></beans:property> <beans:property name="idleConnectionTestPeriod" value="18000"></beans:property> <beans:property name="checkoutTimeout" value="5000"></beans:property> <beans:property name="automaticTestTable" value="t_c3p0"></beans:property> </beans:bean> </beans:beans>
spring-context.xml修改以下
<?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-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"> <!-- 不須要訪問權限 --> <http pattern="/page/login.jsp" security="none"></http> <http auto-config="false"> <form-login login-page="/page/login.jsp" default-target-url="/page/admin.jsp" authentication-failure-url="/page/login.jsp?error=true" /> <logout invalidate-session="true" logout-success-url="/page/login.jsp" logout-url="/j_spring_security_logout" /> <!-- 經過配置custom-filter來增長過濾器,before="FILTER_SECURITY_INTERCEPTOR"表示在SpringSecurity默認的過濾器以前執行。 --> <custom-filter ref="filterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR" /> </http> <!-- 認證過濾器 --> <beans:bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor"> <!-- 用戶擁有的權限 --> <beans:property name="accessDecisionManager" ref="accessDecisionManager" /> <!-- 用戶是否擁有所請求資源的權限 --> <beans:property name="authenticationManager" ref="authenticationManager" /> <!-- 資源與權限對應關係 --> <beans:property name="securityMetadataSource" ref="securityMetadataSource" /> </beans:bean> <!-- 受權管理器 --> <beans:bean id="accessDecisionManager" class="com.sunny.auth.MyAccessDecisionManager"> </beans:bean> <!--自定義的切入點--> <beans:bean id="securityMetadataSource" class="com.sunny.auth.MyFilterInvocationSecurityMetadataSource"> <beans:property name="builder" ref="builder"></beans:property> </beans:bean> <beans:bean id="builder" class="com.sunny.auth.JdbcRequestMapBulider"> <beans:property name="dataSource" ref="dataSource" /> <beans:property name="resourceQuery" value="select re.res_string,r.name from role r,resc re,resc_role rr where r.id=rr.role_id and re.id=rr.resc_id" /> </beans:bean> <!--認證管理--> <authentication-manager alias="authenticationManager"> <authentication-provider> <jdbc-user-service data-source-ref="dataSource" id="usersService" users-by-username-query="select username,password,status as enabled from user where username = ?" authorities-by-username-query="select user.username,role.name from user,role,user_role where user.id=user_role.user_id and user_role.role_id=role.id and user.username=?" /> </authentication-provider> </authentication-manager> </beans:beans>
1.http中的custom-filter是特別要注意的,就是經過這個標籤來增長過濾器的,其中before="FILTER_SECURITY_INTERCEPTOR"表示在SpringSecurity默認的過濾器以前執行。
package com.sunny.auth; import java.util.Collection; import java.util.Iterator; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; public class MyAccessDecisionManager implements AccessDecisionManager{ /* * 該方法決定該權限是否有權限訪問該資源,其實object就是一個資源的地址,authentication是當前用戶的 * 對應權限,若是沒登錄就爲遊客,登錄了就是該用戶對應的權限 */ @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { if(configAttributes == null) { return; } //所請求的資源擁有的權限(一個資源對多個權限) Iterator<ConfigAttribute> iterator = configAttributes.iterator(); while(iterator.hasNext()) { ConfigAttribute configAttribute = iterator.next(); //訪問所請求資源所須要的權限 String needPermission = configAttribute.getAttribute(); System.out.println("訪問"+object.toString()+"須要的權限是:" + needPermission); //用戶所擁有的權限authentication Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); for(GrantedAuthority ga : authorities) { if(needPermission.equals(ga.getAuthority())) { return; } } } //沒有權限 throw new AccessDeniedException("沒有權限訪問! "); } @Override public boolean supports(ConfigAttribute attribute) { return true; } @Override public boolean supports(Class<?> clazz) { return true; } }
pom.xml中增長
<!-- j2ee --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> </dependency>
web.xml默認頁面仍然是
admin能訪問的頁面有admin.jsp、user.jsp;
user能訪問的有user.jsp;
test能訪問的有test.jsp。
若是權限不足,會提示「沒有權限訪問!」