Shiro的Filter機制詳解---源碼分析

Shiro的Filter機制詳解

首先從spring-shiro.xml的filter配置提及,先回答兩個問題:html

1, 爲何相同url規則,後面定義的會覆蓋前面定義的(執行的時候只執行最後一個)。web

2, 爲何兩個url規則均可以匹配同一個url,只執行第一個呢。spring

 

下面分別從這兩個問題入手,最終閱讀源碼獲得解答。apache

問題一解答

相同url但定義在不一樣的行,後面覆蓋前面session

app

/usr/login.do=test3
/usr/login.do=test1,test2
不會執行test3的filter

要解答第一個問題,須要知道shiro(或者說是spring)是如何掃描這些url規則並保存的。框架

Web.xml配置shiro以及spring-shiro.xml的核心配置

在web.xml中定義shiroFilter
複製代碼
<filter>
        <filter-name>shiroFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <init-param>
            <!-- 該值缺省爲false,表示生命週期由SpringApplicationContext管理,設置爲true則表示由ServletContainer管理 -->
            <param-name>targetFilterLifecycle</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>shiroFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
複製代碼
在spring-shiro.xml中定義shiroFilter

(要和web.xml中的名稱同樣,由於spring就是依靠名稱來獲取這個bean的) jsp

複製代碼
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager" />
        <property name="loginUrl" value="/login.jsp" />
        <property name="unauthorizedUrl" value="/WEB-INF/405.html" />
        <property name="filters">
            <map>
                <entry key="kickout" value-ref="kickoutSessionControlFilter" />
            </map>
        </property>
        <property name="filterChainDefinitions">
            <value>
                /**=kickout
                /usr/login.do=anon
                /security/*=anon
                /usr/login.do=authc
                /usr/test/*=authc
            </value>
        </property>
    </bean>
複製代碼

都定義好以後,分析org.springframework.web.filter.DelegatingFilterProxy發現該filter類的任務是:將具體工做分派給org.apache.shiro.spring.web.ShiroFilterFactoryBean這個類中的靜態內部類SpringShiroFilter作。函數

具體spring內部是怎麼將工做委派的,暫時沒有分析。post

如今關注的是當spring把具體工做委派給ShiroFilterFactoryBean後,該類是怎麼工做的。

Spring將配置注入到ShiroFilterFactoryBean

在這以前,spring經過bean注入,將ShiroFilterFactoryBean的相關成員經過set方法注入進去。

前面已經配置了filters和filterChainDefinitions,再次貼出以下所示:

複製代碼
<property name="filters">
            <map>
                <entry key="kickout" value-ref="kickoutSessionControlFilter" />
            </map>
        </property>
        <property name="filterChainDefinitions">
            <value>
                /**=kickout
                /usr/login.do=anon
                /security/*=anon
                /usr/login.do=authc
                /usr/test/*=authc
            </value>
        </property>
複製代碼

看一下ShiroFilterFactoryBean是怎麼接收他們的。

 

Filters很簡單,只須要map接收就自動完成了。

public void setFilters(Map<String, Filter> filters) {
        this.filters = filters;
    }

可是filterChainDefinitions是String類型的,須要轉換(使用了ini轉換方法,內部使用LinkedHashMap保存url和filter的映射關係,保證了順序)

複製代碼
public void setFilterChainDefinitions(String definitions) {
        Ini ini = new Ini();
        ini.load(definitions);
        //did they explicitly state a 'urls' section?  Not necessary, but just in case:
        Ini.Section section = ini.getSection(IniFilterChainResolverFactory.URLS);
        if (CollectionUtils.isEmpty(section)) {
            //no urls section.  Since this _is_ a urls chain definition property, just assume the
            //default section contains only the definitions:
            section = ini.getSection(Ini.DEFAULT_SECTION_NAME);
        }
        setFilterChainDefinitionMap(section);
    }
複製代碼

這兩步完成後,filters被注入

filterChianDefinitions也被注入,可是注入方法經過shiro自定義了ini方式,

該方式經過LinkedHashMap保存url規則和對應的權限(鍵值對),因此當寫了相同的url規則,後者會覆蓋前者(------如今對HashMap的存儲規則遺忘了,須要再看一下)

問題一解答完成

 

問題二解答:

同一個url能夠匹配不一樣的規則,但只執行首行
/usr/* =test1,test2
/usr/login.do=test3
url = /usr/login.do請求來了,不會執行test3,由於已經匹配了/usr/* =test1,test2
要解答該問題,須要知道每一個url的FilterChain是如何獲取的

接上分析:

有了filter和filterChainDefinitionMap的數據後,下面的工做是構造FilterChainManager

構造FilterChainManager

爲何到這一步呢?

查看spring委託機制,最終找到ShiroFilterFactoryBean的createInstance()方法(這個方法是shiro的filter構造機制的主線),因爲ShiroFilterFactoryBean 實現了FactoryBean,spring就是經過這個接口的createInstance方法獲取到filter實例的,下面是該方法在ShiroFilterFactoryBean中的實現:

複製代碼
protected AbstractShiroFilter createInstance() throws Exception {
        log.debug("Creating Shiro Filter instance.");
        SecurityManager securityManager = getSecurityManager();
        if (securityManager == null) {
            String msg = "SecurityManager property must be set.";
            throw new BeanInitializationException(msg);
        }
        if (!(securityManager instanceof WebSecurityManager)) {
            String msg = "The security manager does not implement the WebSecurityManager interface.";
            throw new BeanInitializationException(msg);
        }
        FilterChainManager manager = createFilterChainManager();
       
        PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
        chainResolver.setFilterChainManager(manager);
       
        return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
    }
複製代碼

從這裏能夠知道,首先獲取filterChainManager,具體方法以下

複製代碼
protected FilterChainManager createFilterChainManager() {
        DefaultFilterChainManager manager = new DefaultFilterChainManager();
        Map<String, Filter> defaultFilters = manager.getFilters();
        //apply global settings if necessary:
        for (Filter filter : defaultFilters.values()) {
            applyGlobalPropertiesIfNecessary(filter);
        }
        //Apply the acquired and/or configured filters:
        Map<String, Filter> filters = getFilters();
        if (!CollectionUtils.isEmpty(filters)) {
            for (Map.Entry<String, Filter> entry : filters.entrySet()) {
                String name = entry.getKey();
                Filter filter = entry.getValue();
                applyGlobalPropertiesIfNecessary(filter);
                if (filter instanceof Nameable) {
                    ((Nameable) filter).setName(name);
                }
                //'init' argument is false, since Spring-configured filters should be initialized
                //in Spring (i.e. 'init-method=blah') or implement InitializingBean:
                manager.addFilter(name, filter, false);
            }
        }
        //build up the chains:
        Map<String, String> chains = getFilterChainDefinitionMap();
        if (!CollectionUtils.isEmpty(chains)) {
            for (Map.Entry<String, String> entry : chains.entrySet()) {
                String url = entry.getKey();
                String chainDefinition = entry.getValue();
                manager.createChain(url, chainDefinition);
            }
        }
        return manager;
    }
複製代碼

分析後得知,首先在createFilterChainManager()方法中,建立一個DefaultFilterChainManager對象,而這個對象的構造函數在最後會將DefaultFilter中定義的shiro默認的filter映射加入到該對象中。以下代碼就是DefaultFilter的定義。

 

在DefaultFilterChainManager中還作了一件事就是url-filter的映射變成filterChain,這句代碼就是執行這個任務(將咱們在xml文件中定義的filterChainDefinitions變成filterChain)。

manager.createChain(url, chainDefinition);

做用是將權限分割:如

"authc, roles[admin,user], perms[file:edit]"

將會被分割爲

{ "authc", "roles[admin,user]", "perms[file:edit]" }

具體的源代碼以下:

複製代碼
public void createChain(String chainName, String chainDefinition) {
      //。。。。。。。。

        //parse the value by tokenizing it to get the resulting filter-specific config entries
        //
        //e.g. for a value of
        //
        //     "authc, roles[admin,user], perms[file:edit]"
        //
        // the resulting token array would equal
        //
        //     { "authc", "roles[admin,user]", "perms[file:edit]" }
        //
        String[] filterTokens = splitChainDefinition(chainDefinition);
        for (String token : filterTokens) {
            String[] nameConfigPair = toNameConfigPair(token);
            addToChain(chainName, nameConfigPair[0], nameConfigPair[1]);
        }
    }
複製代碼

而且經過toNameConfigPair(token)將如:roles[admin,user]形式的變成roles,admin,user形式的分割

而後根據url規則 映射 權限和角色

能夠發現,每次分割一個token,都會經過addToChain方法接受

分析public void addToChain(String chainName, String filterName, String chainSpecificFilterConfig)方法

複製代碼
public void addToChain(String chainName, String filterName, String chainSpecificFilterConfig) {
        if (!StringUtils.hasText(chainName)) {
            throw new IllegalArgumentException("chainName cannot be null or empty.");
        }
        Filter filter = getFilter(filterName);
        if (filter == null) {
            throw new IllegalArgumentException("There is no filter with name '" + filterName +
                    "' to apply to chain [" + chainName + "] in the pool of available Filters.  Ensure a " +
                    "filter with that name/path has first been registered with the addFilter method(s).");
        }

        applyChainConfig(chainName, filter, chainSpecificFilterConfig);

        NamedFilterList chain = ensureChain(chainName);
        chain.add(filter);
    }
複製代碼

分析applyChainConfig(chainName, filter, chainSpecificFilterConfig);

複製代碼
protected void applyChainConfig(String chainName, Filter filter, String chainSpecificFilterConfig) {
      //………………………….
        if (filter instanceof PathConfigProcessor) {
            ((PathConfigProcessor) filter).processPathConfig(chainName, chainSpecificFilterConfig);
        } else {
            if (StringUtils.hasText(chainSpecificFilterConfig)) {
                //they specified a filter configuration, but the Filter doesn't implement PathConfigProcessor
                //this is an erroneous config:
                String msg = "chainSpecificFilterConfig was specified, but the underlying " +
                        "Filter instance is not an 'instanceof' " +
                        PathConfigProcessor.class.getName() + ".  This is required if the filter is to accept " +
                        "chain-specific configuration.";
                throw new ConfigurationException(msg);
            }
        }
}
複製代碼

因爲咱們自定義的filter都是PathMatchingFilter的子類,因此在applyChainConfig方法中完成的就是將url添加到filter的url表中。

在PathMatchingFilter中能夠發現

protected Map<String, Object> appliedPaths = new LinkedHashMap<String, Object>();

processPathConfig 方法的實現以下

複製代碼
public Filter processPathConfig(String path, String config) {
        String[] values = null;
        if (config != null) {
            values = split(config);
        }
        this.appliedPaths.put(path, values);
        return this;
}
複製代碼

基本上在spring-shiro.xml中定義filter的載入過程已經閱讀完成,

1, 定義一個DefaultFilterChainManager對象

2, 首先加載默認的filter

3, 加載xml文件中定義的filter

4, 加載xml文件定義的url和filter映射關係

5, 將映射關係解析爲以url爲鍵,NamedFilterList爲值的鍵值對。

6, 在解析的過程當中,對每一個url和對應的過濾條件,都會放到對應filter的appliedPaths中(在PathMatchingFilter中定義)。

如今FilterChainManager的對象已經建立完畢,而且每一個filter也已經實例化完畢。

構造SpingShiroFilter

在建立SpringShiroFilter以前還要將剛纔建立的FilterChainManager對象包裝成一個PathMatchingFilterChainResolver對象(註釋的意思是:不直接將FilterChainManager對象暴露給AbstractShiroFilter的實現者,在這裏就是SpringShiroFilter。)

 

 

PathMatchingFilterChainResolver最重要的做用是:當請求url來的時候,他擔任匹配工做(調用該類的getChain方法作匹配,暫時先不分析該方法,等知道在哪裏調用該方法時候再分析。其實問題二此時已經能夠解答,經過該方法就能夠知道,某個url匹配到過濾鏈的第一個規則時就return了。)

上圖最後一句話執行完成後,一個SpringShiroFilter建立完畢。

請求過濾過程分析(上)

下面分析當url請求到來的時候,shiro是如何完成過濾的。首先經過圖片大體的瞭解一下。

 

 

如今分析AbstractShiroFilter的doFilterInternal()方法

複製代碼
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
            throws ServletException, IOException {
        Throwable t = null;
        try {
            final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
            final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
            final Subject subject = createSubject(request, response);
            subject.execute(new Callable() {
                public Object call() throws Exception {
                    updateSessionLastAccessTime(request, response);
                    executeChain(request, response, chain);
                    return null;
                }
            });
        } catch (ExecutionException ex) {
            t = ex.getCause();
        } catch (Throwable throwable) {
            t = throwable;
        }
        //…………
    }
複製代碼

暫時不關心subject相關的建立等過程,只關心這行代碼

executeChain(request, response, chain);

具體實現以下

protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain)
            throws IOException, ServletException {
        FilterChain chain = getExecutionChain(request, response, origChain);
        chain.doFilter(request, response);
}

再看getExecutionChain(request, response, origChain);具體實現以下:

複製代碼
protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
        FilterChain chain = origChain;

        FilterChainResolver resolver = getFilterChainResolver();
        if (resolver == null) {
            log.debug("No FilterChainResolver configured.  Returning original FilterChain.");
            return origChain;
        }

        FilterChain resolved = resolver.getChain(request, response, origChain);
        if (resolved != null) {
            log.trace("Resolved a configured FilterChain for the current request.");
            chain = resolved;
        } else {
            log.trace("No FilterChain configured for the current request.  Using the default.");
        }

        return chain;
    }
複製代碼

能夠發現,這裏用到了咱們在建立SpringShiroFilter時傳遞的FilterChainResolver,至此,咱們終於找到了getChain()方法在這裏被調用了。其源碼實現以下

複製代碼
public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
        FilterChainManager filterChainManager = getFilterChainManager();
        if (!filterChainManager.hasChains()) {
            return null;
        }

        String requestURI = getPathWithinApplication(request);

        //the 'chain names' in this implementation are actually path patterns defined by the user.  We just use them
        //as the chain name for the FilterChainManager's requirements
        for (String pathPattern : filterChainManager.getChainNames()) {

            // If the path does match, then pass on to the subclass implementation for specific checks:
            if (pathMatches(pathPattern, requestURI)) {
                if (log.isTraceEnabled()) {
                    log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "].  " +
                            "Utilizing corresponding filter chain...");
                }
                return filterChainManager.proxy(originalChain, pathPattern);
            }
        }

        return null;
    }
複製代碼

從for循環能夠看出,當匹配到第一個url規則,則return一個表明這個url規則的FilterChain給web容器執行。

 

問題二解答:每一個url在匹配他的FilterChain時,當匹配到第一個URL規則時,就返回。

 

 

請求過濾過程分析(下)

FilterChain的實現類爲org.apache.shiro.web.servlet.ProxiedFilterChain

從該類的doFilter方法能夠知道,它會將Filter鏈的Filter的doFilter方法順序執行一遍。下圖展現了這一過程

 

如今只須要分析每一個Filter的doFilter方法就好了。

先看一下shiro整個filter框架繼承關係(圖片來自第八章 攔截器機制——《跟我學Shiro》)

 

上面是它的繼承關係:最終的doFilter方法在OncePerRequestFilter中實現,具體代碼以下:

複製代碼
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
        if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
            log.trace("Filter '{}' already executed.  Proceeding without invoking this filter.", getName());
            filterChain.doFilter(request, response);
        } else //noinspection deprecation
            if (/* added in 1.2: */ !isEnabled(request, response) ||
                /* retain backwards compatibility: */ shouldNotFilter(request) ) {
            log.debug("Filter '{}' is not enabled for the current request.  Proceeding without invoking this filter.",
                    getName());
            filterChain.doFilter(request, response);
        } else {
            // Do invoke this filter...
            log.trace("Filter '{}' not yet executed.  Executing now.", getName());
            request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
            try {
                doFilterInternal(request, response, filterChain);
            } finally {
                // Once the request has finished, we're done and we don't
                // need to mark as 'already filtered' any more.
                request.removeAttribute(alreadyFilteredAttributeName);
            }
        }
    }
複製代碼

能夠發現該方法最終會調用doFilterInternal(request, response, filterChain);來完成具體的過濾操做,doFilterInternal方法在 SpringShiroFilter的直接父類AbstractShiroFilter的具體實現過程已經在上面分析過了:具體的就是shiro真正驗證受權前的subject,session等初始化的工做,使得後面的過濾以及驗證受權工做能夠獲得subject等而後正常工做。完成後調用其餘shiro filter進行繼續過濾

 

而除了shiroFilter以外,其他的filter都是AdviceFilter分支的子類。剛纔看了AbstractShiroFilter的doFilterInternal方法,如今看一下AdviceFilter對該方法的實現:

複製代碼
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        Exception exception = null;
        try {
            boolean continueChain = preHandle(request, response);
            if (log.isTraceEnabled()) {
                log.trace("Invoked preHandle method.  Continuing chain?: [" + continueChain + "]");
            }
            if (continueChain) {
                executeChain(request, response, chain);
            }
            postHandle(request, response);
            if (log.isTraceEnabled()) {
                log.trace("Successfully invoked postHandle method");
            }
        } catch (Exception e) {
            exception = e;
        } finally {
            cleanup(request, response, exception);
        }
    }
複製代碼

與AbstractShiroFilter的doFilterInternal方法不一樣的是,這裏經過continueChain變量來判斷到底後續的filter會不會被繼續執行。而該變量的值由preHandle()函數決定。

 

基本上全部在系統中用到的filter都是繼承PathMatchingFilter類的。看一下該類的preHandle()函數實現,能夠發現,咱們在xml文件中定義的url匹配,在這裏面能夠看到匹配原則了:

複製代碼
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        if (this.appliedPaths == null || this.appliedPaths.isEmpty()) {
            if (log.isTraceEnabled()) {
                log.trace("appliedPaths property is null or empty.  This Filter will passthrough immediately.");
            }
            return true;
        }
        for (String path : this.appliedPaths.keySet()) {
            // If the path does match, then pass on to the subclass implementation for specific checks
            //(first match 'wins'):
            if (pathsMatch(path, request)) {
                log.trace("Current requestURI matches pattern '{}'.  Determining filter chain execution...", path);
                Object config = this.appliedPaths.get(path);
                return isFilterChainContinued(request, response, path, config);
            }
        }
        //no path matched, allow the request to go through:
        return true;
    }
複製代碼

繼續調用isFilterChainContinued(request, response, path, config)--> onPreHandle(request, response, pathConfig);

分析onPreHandle(),PathMatchingFilter本身並無實現,只是簡單的返回true。因此當咱們自定義filter的時候,要將具體的邏輯實如今該方法中,或者實現該類的子類AccessControlFilter(該類對onPreHandle()方法進行了更細緻的劃分,大部分通常會繼承該類)

有興趣的能夠分析一下shiro自帶的這些filter

相關文章
相關標籤/搜索