【Spring Security】5、自定義過濾器

在以前的幾篇security教程中,資源和所對應的權限都是在xml中進行配置的,也就在http標籤中配置intercept-url,試想要是配置的對象很少,那還好,可是日常實際開發中都每每是很是多的資源和權限對應,並且寫在配置文件裏面寫改起來還得該源碼配置文件,這顯然是很差的。所以接下來,將用數據庫管理資源和權限的對應關係。數據庫仍是接着以前的,用mysql數據庫,所以也不用另外引入額外的jar包。java

1、數據庫表的設計

數據庫要提供給security的數據無非就是, 資源(說的通俗點就是範圍資源地址)和對應的權限,這裏就有兩張表,可是由於他們倆是多對多的關係,所以還要設計一張讓這兩張表 關聯起來的表,除此以外,還有一張 用戶表,有由於用戶和 角色也是多對多的關係,還要額外加一張 用戶和角色關聯的表。這樣總共下來就是五張表。下面就是對應的模型圖:

 

建表和添加數據的sql語句:
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);

 

user表中包含用戶登錄信息,role角色表中包含受權信息,resc資源表中包含須要保護的資源。

2、實現從數據庫中讀取資源信息

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

三 構建一個數據庫的操做的類

雖然上述的數據符合security的須要,可是security將這種數據類型進行了封裝,把它封裝成 Map<RequestMatcher, Collection<ConfigAttribute>>這樣的類型,其中RequestMatcher接口就是咱們數據庫中的res_string,其實現類爲AntPathRequestMatcher,構建一個這樣的對象只要在new的時候傳入res_string就能夠了,Collection<ConfigAttribute>這裏對象構建起來就也是相似的,構建一個ConfigAttribute對象只須要在其實現類SecurityConfig建立的時候傳入角色的名字就能夠。代碼以下:
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;
        }
    }
}

說明:數據庫

  • resourceQuery是查詢數據的sql語句,該屬性在配置bean的時候傳入便可。
  • 內部建立了一個resource來封裝數據。
  • getRequestMatcher方法就是用來建立RequestMatcher對象的
  • buildRequestMap方法用來最後拼接成LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>>security使用。

4、替換原有功能的切入點

在將這部以前,先得了解大概下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跟蹤得出,其實資源權限關係就放在DefaultFilterInvocationSecurityMetadataSourcerequestMap,中的,這個requestMap就是咱們JdbcRequestMapBulider.buildRequestMap()方法所須要的數據類型,所以就想到了咱們自定義一個類繼承FilterInvocationSecurityMetadataSource接口,將數據查出的數據放到requestMap中去。制定類MyFilterInvocationSecurityMetadataSource繼承FilterInvocationSecurityMetadataSourceInitializingBean接口。緩存

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;
    }
}

說明:服務器

  • requestMap這個屬性就是用來存放資源權限的集合
  • builderJdbcRequestMapBulider類型,用來查找數據庫權限和資源關係
  • 其餘的代碼中都有詳細的註釋

5、配置文件

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&amp;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默認的過濾器以前執行。

2.在配置 builder時候, resourceQuery就是要查詢的sql語句, dataSource爲數據源。
3.在配置認證過濾器的時候, accessDecisionManagerauthenticationManagersecurityMetadataSource這三個屬性是必填項,若缺失會報錯。其中 authenticationManager就是 authentication-manager標籤, securityMetadataSource是自定義的 MyFilterInvocationSecurityMetadataSourceauthenticationManager這裏尚未定義,所以再建立一個類叫 MyAccessDecisionManager,代碼以下:
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。

 

若是權限不足,會提示「沒有權限訪問!」

相關文章
相關標籤/搜索