1. 程式人生 > >Spring Security教程(五):自定義過濾器從資料庫從獲取資源資訊

Spring Security教程(五):自定義過濾器從資料庫從獲取資源資訊

 

在之前的幾篇security教程中,資源和所對應的許可權都是在xml中進行配置的,也就在http標籤中配置intercept-url,試想要是配置的物件不多,那還好,但是平常實際開發中都往往是非常多的資源和許可權對應,而且寫在配置檔案裡面寫改起來還得該原始碼配置檔案,這顯然是不好的。因此接下來,將用資料庫管理資源和許可權的對應關係。資料庫還是接著之前的,用mysql資料庫,因此也不用另外引入額外的jar包。

一、資料庫表的設計

資料庫要提供給security的資料無非就是,資源(說的通俗點就是範圍資源地址)和對應的許可權,這裡就有兩張表,但是因為他們倆是多對多的關係,因此還要設計一張讓這兩張表關聯起來的表,除此之外,還有一張使用者表,有因為使用者和角色也是多對多的關係,還要額外加一張使用者和角色關聯的表。這樣總共下來就是五張表。下面就是對應的模型圖:

建表和新增資料的sql語句:

DROP TABLE IF EXISTS `resc`;
CREATE TABLE `resc` (
  `id` bigint(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', '/adminPage.jsp', '管理員頁面'); INSERT INTO `resc` VALUES ('2', '', 'URL', '/index.jsp', ''); INSERT INTO `resc` VALUES ('3', null, 'URL', '/test.jsp', '測試頁面'); --
---------------------------- -- Table structure for resc_role -- ---------------------------- DROP TABLE IF EXISTS `resc_role`; CREATE TABLE `resc_role` ( `resc_id` bigint(20) NOT NULL DEFAULT '0', `role_id` bigint(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` bigint(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` bigint(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', 'admin', '1', '管理員'); INSERT INTO `user` VALUES ('2', 'user', 'user', '1', '使用者'); INSERT INTO `user` VALUES ('3', 'test', 'test', '1', '測試'); -- ---------------------------- -- Table structure for user_role -- ---------------------------- DROP TABLE IF EXISTS `user_role`; CREATE TABLE `user_role` ( `user_id` bigint(20) NOT NULL DEFAULT '0', `role_id` bigint(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資源表中包含需要保護的資源。

二、實現從資料庫中讀取資源資訊

Spring Security需要的資料無非就是pattern和access類似鍵值對的資料,就像配置檔案中寫的那樣:

<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語句就可以:

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

在資料中執行這條語句做測試,得到如下結果:

 


這樣的格式正是security所需要的資料。

三、構建一個數據庫的操作的類

雖然上述的資料符合security的需要,但是security將這種資料型別進行了封裝,把它封裝成Map<RequestMatcher, Collection<ConfigAttribute>>這樣的型別,其中RequestMatcher介面就是我們資料庫中的res_string,其實現類為AntPathRequestMatcher,構建一個這樣的物件只要在new的時候傳入res_string就可以了,Collection<ConfigAttribute>這裡物件構建起來就也是類似的,構建一個ConfigAttribute物件只需要在其實現類SecurityConfig建立的時候傳入角色的名字就可以。程式碼如下:

package com.zmc.demo;
 
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.access.intercept.DefaultFilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.AntPathRequestMatcher;
import org.springframework.security.web.util.RequestMatcher;
 
 
/**
 * @classname JdbcRequestMapBulider
 * @author ZMC
 * @time 2017-1-10
 * 查詢資源和角色,並構建RequestMap
 */
public class JdbcRequestMapBulider
    extends JdbcDaoSupport{
    //查詢資源和許可權關係的sql語句
    private String resourceQuery = "";
    
    public String getResourceQuery() {
        return resourceQuery;
    }
 
    //查詢資源
    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 void setResourceQuery(String resourceQuery) {
        this.resourceQuery = resourceQuery;
    }
    
    /**
     * @classname Resource
     * @author ZMC
     * @time 2017-1-10
     * 資源內部類
     */
    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;
        }
    }
    
}
 

說明:

  1. resourceQuery是查詢資料的sql語句,該屬性在配置bean的時候傳入即可。
  2. 內部建立了一個resource來封裝資料。
  3. getRequestMatcher方法就是用來建立RequestMatcher物件的
  4. buildRequestMap方法用來最後拼接成LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>>共security使用。

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

在講這部分之前,先得了解大概下security的執行過程,我們知道Spring Security核心就是一系列的過濾器鏈。當一個請求來的時候,首先要通過過濾器鏈的校驗,校驗通過之後才會訪問使用者各種資訊。 

當用戶登陸的時候,會被AuthenticationProcessingFilter攔截,呼叫AuthenticationManager的實現類。

同時AuthenticationManager會呼叫ProviderManager來獲取使用者驗證資訊,其中不同的Provider呼叫的服務不同,因為這些資訊可以是在資料庫上,可以是在LDAP伺服器上,可以是xml配置檔案上等,這個例子中就是為資料庫。

如果驗證通過後會將使用者的許可權資訊放到spring的全域性快取SecurityContextHolder中,以備後面訪問資源時使用。

當訪問資源,訪問url時,會通過AbstractSecurityInterceptor攔截器攔截,其中會呼叫FilterInvocationSecurityMetadataSource的方法來獲取被攔截url所需的全部許可權,其中FilterInvocationSecurityMetadataSource的常用的實現類為DefaultFilterInvocationSecurityMetadataSource,這個類中有個很關鍵的東西就是requestMap,也就是我們上面所得到的資料,在呼叫授權管理器AccessDecisionManager,這個授權管理器會通過spring的全域性快取SecurityContextHolder獲取使用者的許可權資訊,還會獲取被攔截的url和被攔截url所需的全部許可權,然後根據所配的策略,如果許可權足夠,則返回,許可權不夠則報錯並呼叫許可權不足頁面。

根據原始碼debug跟蹤得出,其實資源許可權關係就放在DefaultFilterInvocationSecurityMetadataSource的requestMap,中的,這個requestMap就是我們JdbcRequestMapBulider.buildRequestMap()方法所需要的資料型別,因此,順氣自然就想到了我們自定義一個類繼承FilterInvocationSecurityMetadataSource介面,將資料查出的資料放到requestMap中去。制定類MyFilterInvocationSecurityMetadataSource繼承FilterInvocationSecurityMetadataSource和InitializingBean介面。

更多流程參閱:Spring Security教程(八):使用者認證流程原始碼詳解

package com.zmc.demo;
 
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;
 
 
/**
 * @classname MyFilterInvocationSecurityMetadataSource
 * @author ZMC
 * @time 2017-1-10
 */
public class MyFilterInvocationSecurityMetadataSource implements
        FilterInvocationSecurityMetadataSource, InitializingBean {
 
    private final static List<ConfigAttribute> NULL_CONFIG_ATTRIBUTE = null;
    // 資源許可權集合
    private Map<RequestMatcher, Collection<ConfigAttribute>> requestMap;
    
    //查詢資料庫許可權和資源關係
    private JdbcRequestMapBulider builder;
    
    /*
     * (non-Javadoc)
     * @see
     * org.springframework.security.access.SecurityMetadataSource#getAttributes
     * (java.lang.Object)
     * 更具訪問資源的地址查詢所需要的許可權
     */
    @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;
    }
 
    /*
     * (non-Javadoc)
     * 
     * @see org.springframework.security.access.SecurityMetadataSource#
     * getAllConfigAttributes()
     * 獲取所有的許可權
     */
    @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;
    }
    /*
     * (non-Javadoc)
     * 
     * @see
     * org.springframework.security.access.SecurityMetadataSource#supports(java
     * .lang.Class)
     */
    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
    //繫結requestMap
    protected Map<RequestMatcher, Collection<ConfigAttribute>> bindRequestMap() {
        
        return builder.buildRequestMap();
    }
 
    /*
     * (non-Javadoc)
     * 
     * @see
     * org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        this.requestMap = this.bindRequestMap();
    }
 
    public void refreshResuorceMap() {
        this.requestMap = this.bindRequestMap();
    }
 
    //get方法
    public JdbcRequestMapBulider getBuilder() {
        return builder;
    }
    
    //set方法
    public void setBuilder(JdbcRequestMapBulider builder) {
        this.builder = builder;
    }
 
}

說明:

  1. requestMap這個屬性就是用來存放資源許可權的集合
  2. builder為JdbcRequestMapBulider型別,用來查詢資料庫許可權和資源關係
  3. 其他的程式碼中都有詳細的註釋

四、配置

<?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="/login.jsp" security="none"></http>
    <http auto-config="false">
        <form-login login-page="/login.jsp" default-target-url="/index.jsp"
            authentication-failure-url="/login.jsp?error=true" />
        <logout invalidate-session="true" logout-success-url="/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="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/springsecuritydemo?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:bean id="builder" class="com.zmc.demo.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>
 
    
    <!-- 認證過濾器 -->
    <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 class="com.zmc.demo.MyAccessDecisionManager" id="accessDecisionManager">
    </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:bean id="securityMetadataSource"
        class="com.zmc.demo.MyFilterInvocationSecurityMetadataSource">
        <beans:property name="builder" ref="builder"></beans:property>
    </beans:bean>
    
    
</beans:beans>

1.http中的custom-filter是特別要注意的,就是通過這個標籤來增加過濾器的,其中before="FILTER_SECURITY_INTERCEPTOR"表示在SpringSecurity預設的過濾器之前執行。
2.在配置builder時候,resourceQuery就是要查詢的sql語句,dataSource為資料來源。其他的如authenticationManager在之前的部落格配置中就有詳細講解。
3.在配置認證過濾器的時候,accessDecisionManager,authenticationManager,securityMetadataSource這三個屬性是必填項,若缺失會報錯。其中authenticationManager就是authentication-manager標籤,securityMetadataSource
是自定義的MyFilterInvocationSecurityMetadataSource,authenticationManager這裡還沒有定義,因此再建立一個類叫MyAccessDecisionManager,程式碼如下:

package com.zmc.demo;
 
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;
 
/**
 * @classname MyAccessDecisionManager
 * @author ZMC
 * @time 2017-1-10
 * 
 */
public class MyAccessDecisionManager implements AccessDecisionManager  {
    
    
    
    /* (non-Javadoc)
     * @see org.springframework.security.access.AccessDecisionManager#decide(org.springframework.security.core.Authentication, java.lang.Object, java.util.Collection)
     * 該方法決定該許可權是否有許可權訪問該資源,其實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) {
        // TODO Auto-generated method stub
        return true;
    }
 
    @Override
    public boolean supports(Class<?> clazz) {
        // TODO Auto-generated method stub
        return true;
    }
 
}

五、結果

使用者對應的角色,和角色能訪問的資源

admin能訪問的頁面有adminPage.jsp、index.jsp;user能訪問的有index.jsp;test能訪問的有test.jsp。

先測試admin使用者:

user使用者測試:

test使用者測試: