1. 程式人生 > >ShiroFilterFactoryBean原始碼及阻截原理深入分析

ShiroFilterFactoryBean原始碼及阻截原理深入分析

Shiro提供了與Web整合的支援,其通過一個ShiroFilter入口來攔截需要安全控制的URL,然後進行相應的控制,ShiroFilter類似於如Strut2/SpringMVC這種web框架的前端控制器,其是安全控制的入口點,其負責讀取配置(如ini配置檔案),然後判斷URL是否需要登入/許可權等工作。

而要在Spring中使用Shiro的話,可在web.xml中配置一個DelegatingFilterProxyDelegatingFilterProxy作用是自動到Spring容器查詢名字為shiroFilterfilter-name)的bean並把所有Filter的操作委託給它。

首先是在web.xml

中配置DelegatingFilterProxy

<filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <init-param>
        <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>

配置好DelegatingFilterProxy後,下面只要再把ShiroFilter配置到Spring容器(此處為Spring的配置檔案)即可:

<bean id="shiroFilter"
class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> </bean>

可以看到我們使用了ShiroFilterFactoryBean來建立shiroFilter,這裡用到了Spring中一種特殊的Bean——FactoryBean。當需要得到名為”shiroFilter“的bean時,會呼叫其getObject()來獲取例項。下面我們通過分析ShiroFilterFactoryBean建立例項的過程來探究Shiro是如何實現安全攔截的:

    public Object getObject() throws Exception {
        if (instance == null) {
            instance = createInstance();
        }
        return instance;
    }

其中呼叫了createInstance()來建立例項:

  protected AbstractShiroFilter createInstance() throws Exception {

        // 這裡是通過FactoryBean注入的SecurityManager(必須)
        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);
    }

可以看到建立SpringShiroFilter時用到了兩個元件:SecurityManagerChainResolver

  • SecurityManager:我們知道其在Shiro中的地位,類似於一個“安全大管家”,相當於SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher,是Shiro的心臟,所有具體的互動都通過SecurityManager進行控制,它管理著所有Subject、且負責進行認證和授權、及會話、快取的管理。
  • ChainResolver:Filter鏈解析器,用來解析出該次請求需要執行的Filter鏈。
  • PathMatchingFilterChainResolverChainResolver的實現類,其中還包含了兩個重要元件FilterChainManagerPatternMatcher
  • FilterChainManager:管理著Filter和Filter鏈,配合PathMatchingFilterChainResolver解析出Filter鏈
  • PatternMatcher:用來進行請求路徑匹配,預設為Ant風格的路徑匹配

先有一個大體的瞭解,那麼對於原始碼分析會有不少幫助。下面會對以上兩個重要的元件進行分析,包括PathMatchingFilterChainResolverFilterChainManager。首先貼一段ShiroFilter的在配置檔案中的定義:

<!-- Shiro的Web過濾器 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager" />
        <property name="loginUrl" value="/login" />
        <property name="unauthorizedUrl" value="/special/unauthorized" />
        <property name="filters">
            <util:map>
                <entry key="authc" value-ref="formAuthenticationFilter" />
                <entry key="logout" value-ref="logoutFilter" />
                <entry key="ssl" value-ref="sslFilter"></entry>
            </util:map>
        </property>
        <property name="filterChainDefinitions">
            <value>
                /resources/** = anon
                /plugin/** = anon
                /download/** = anon
                /special/unauthorized = anon
                /register = anon
                /login = ssl,authc
                /logout = logout
                /admin/** = roles[admin]

                /** = user
            </value>
        </property>
    </bean>

再來看看PathMatchingFilterChainResolverFilterChainManager的建立過程:

  protected FilterChainManager createFilterChainManager() {

        // 預設使用的FilterChainManager是DefaultFilterChainManager
        DefaultFilterChainManager manager = new DefaultFilterChainManager();
        // DefaultFilterChainManager預設會註冊的filters(後面會列出)
        Map<String, Filter> defaultFilters = manager.getFilters();

        // 將ShiroFilterFactoryBean配置的一些公共屬性(上面配置的loginUrl,successUrl,unauthorizeUrl)應用到預設註冊的filter上去
        for (Filter filter : defaultFilters.values()) {
            applyGlobalPropertiesIfNecessary(filter);
        }

        // 處理自定義的filter(上面配置的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);
                }
                // 將Filter新增到manager中去,可以看到對於Filter的管理是依賴於FilterChainManager的
                manager.addFilter(name, filter, false);
            }
        }

        // 根據FilterChainDefinition的配置來構建Filter鏈(上面配置的filterChainDefinitions屬性)
        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();
                // 後面會分析該步的原始碼,功能上就是建立Filter鏈
                manager.createChain(url, chainDefinition);
            }
        }

        return manager;
    }

下面有必要來看看DefaultFilterChainManager的原始碼,分析一下上面呼叫到的方法。先來看看他的幾個重要的屬性:

    private FilterConfig filterConfig;

    private Map<String, Filter> filters; //pool of filters available for creating chains

    private Map<String, NamedFilterList> filterChains; //key: chain name, value: chain

其中filterConfig僅在初始化Filter時有效,而我們自定義的Filter都不是init的,所以該屬性可以暫時忽略()。 
而後面兩張map就重要了:filters中快取了所有新增的filter,filterChains則快取了所有的filterChain。其中前者的key是filter name,value是Filter。而後者的key是chain name,value是NamedFilterList。 
有的童鞋可能會問NamedFilterList是怎麼樣的結構呢,你可以把它當成List<Filter>,這樣就好理解了吧。下面再分析剛才createFilterChainManager()中呼叫過的manager的幾個方法:

  • addFilter(快取filter讓manager來管理)
    public void addFilter(String name, Filter filter, boolean init) {
        addFilter(name, filter, init, true);
    }

    protected void addFilter(String name, Filter filter, boolean init, boolean overwrite) {
        Filter existing = getFilter(name);
        if (existing == null || overwrite) {
            if (filter instanceof Nameable) {
                ((Nameable) filter).setName(name);
            }
            if (init) {
                initFilter(filter);
            }
            this.filters.put(name, filter);
        }
    }

filter快取到filters這張map裡,不管是預設註冊的還是自定義的都需要FilterChainManager來統一管理。

  • createChain:建立filterChain並將定義的filter都加進去
    // chainName就是攔截路徑"/resources/**",chainDefinition就是多個過濾器名的字串
    public void createChain(String chainName, String chainDefinition) {
        if (!StringUtils.hasText(chainName)) {
            throw new NullPointerException("chainName cannot be null or empty.");
        }
        if (!StringUtils.hasText(chainDefinition)) {
            throw new NullPointerException("chainDefinition cannot be null or empty.");
        }

        if (log.isDebugEnabled()) {
            log.debug("Creating chain [" + chainName + "] from String definition [" + chainDefinition + "]");
        }

        // 先分離出配置的各個filter,比如 
        // "authc, roles[admin,user], perms[file:edit]" 分離後的結果是:
        // { "authc", "roles[admin,user]", "perms[file:edit]" }
        String[] filterTokens = splitChainDefinition(chainDefinition);

        // 進一步分離出"[]"內的內容,其中nameConfigPair是一個長度為2的陣列
        // 比如 roles[admin,user] 經過解析後的nameConfigPair 為{"roles", "admin,user"}
        for (String token : filterTokens) {
            String[] nameConfigPair = toNameConfigPair(token);

            // 得到了 攔截路徑、filter以及可能的"[]"中的值,那麼執行addToChain
            addToChain(chainName, nameConfigPair[0], nameConfigPair[1]);
        }
    }
  • addToChain
   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).");
        }

        // 將"[]"中的匹配關係註冊到filter中
        applyChainConfig(chainName, filter, chainSpecificFilterConfig);

        // 確保chain已經被加到filterChains這張map中了
        NamedFilterList chain = ensureChain(chainName);
        // 將該filter加入當前chain
        chain.add(filter);
    }

至此,FilterChainManager就建立完了,它無非就是快取了兩張map,沒有什麼邏輯上的操作。下面將FilterChainManager設定到PathMatchingFilterChainResolver中。PathMatchingFilterChainResolver實現了FilterChainResolver介面,該介面中只定義了一個方法:

FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain);

通過解析請求來得到一個新的FilterChain。而PathMatchingFilterChainResolver實現了該介面,依靠了FilterChainManager中儲存的chainFiltersfilters這兩張map來根據請求路徑解析出相應的filterChain,並且和originalChain組合起來使用。下面具體看看PathMatchingFilterChainResolver中的實現:

   public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
        // 得到 FilterChainManager 
        FilterChainManager filterChainManager = getFilterChainManager();
        if (!filterChainManager.hasChains()) {
            return null;
        }

        String requestURI = getPathWithinApplication(request);

        // chainNames就是剛定義的filterChains的keySet,也就是所有的路徑集合(比如:["/resources/**","/login"])
        for (String pathPattern : filterChainManager.getChainNames()) {

            // 請求路徑是否匹配某個 定義好的路徑:
            if (pathMatches(pathPattern, requestURI)) {
                if (log.isTraceEnabled()) {
                    log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "].  " + "Utilizing corresponding filter chain...");
                }
                // 找到第一個匹配的Filter鏈,那麼就返回一個ProxiedFilterChain
                return filterChainManager.proxy(originalChain, pathPattern);
            }
        }

        return null;
    }

這裡返回只有兩種情況,要麼是null,要麼就是一個ProxiedFilterChain。返回null並不表示中斷FilterChain,而是隻用originChain。而關於ProxiedFilterChain,它實現了FilterChain,內部維護了兩份FilterChain(其實一個是FilterChain,另一個是List<Filter>) 
FilterChain也就是web.xml中註冊的Filter形成的FilterChain,我們稱之為originChain。而另一個List<Filter>則是我們在Shiro中註冊的Filter鏈了,下面看看ProxiedFilterChain中關於doFilter(...)的實現:

 public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
        if (this.filters == null || this.filters.size() == this.index) {
            //we've reached the end of the wrapped chain, so invoke the original one:
            if (log.isTraceEnabled()) {
                log.trace("Invoking original filter chain.");
            }
            this.orig.doFilter(request, response);
        } else {
            if (log.isTraceEnabled()) {
                log.trace("Invoking wrapped filter at index [" + this.index + "]");
            }
            this.filters.get(this.index++).doFilter(request, response, this);
        }
    }

可以看到,它會先執行Shiro中執行的filter,然後再執行web.xml中的Filter。不過要注意的是,需要等到originChain執行到ShiroFilter之後才會執行Shiro中的Filter鏈。 
至此,兩個元件的建立過程差不多都介紹完了,那麼當這兩個元件建立完畢後,是如何工作的呢? 
先從ShiroFilter入手,因為它是總的攔截器,看看其中的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);

            //noinspection unchecked
            subject.execute(new Callable() {
                public Object call() throws Exception {
                    // 其實需要關心的就在這裡
                    // touch一下session
                    updateSessionLastAccessTime(request, response);
                    // 執行Filter鏈
                    executeChain(request, response, chain);
                    return null;
                }
            });
        } catch (ExecutionException ex) {
            t = ex.getCause();
        } catch (Throwable throwable) {
            t = throwable;
        }

        if (t != null) {
            if (t instanceof ServletException) {
                throw (ServletException) t;
            }
            if (t instanceof IOException) {
                throw (IOException) t;
            }
            //otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one:
            String msg = "Filtered request failed.";
            throw new ServletException(msg, t);
        }
    }

跟進executeChain(...)方法:

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

如何得到FilterChain的呢?如果你認真的看到這裡,那麼你應該不難想到其中肯定利用了剛才註冊的ChainResolver

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

猜對了~並且也驗證了當resolver.getChain(...)返回null時,直接使用originChain了。然後執行返回的FilterChaindoFilter(...)方法。這個過程我們再脫離程式碼來分析一下:當我們從瀏覽器發出一個請求,究竟發生了什麼? 
這裡只站在Filter的層面來分析。伺服器啟動後,讀取web.xml中的filterfilter-mapping節點後組成FilterChain,對請求進行攔截。攔截的順序按照filter節點的定義順序,Shiro利用ShiroFilter來充當一個總的攔截器來分發所有需要被Shiro攔截的請求,所以我們看到在Shiro中我們還可以自定義攔截器。ShiroFilter根據它在攔截器中的位置,只要執行到了那麼就會暫時中斷原FilterChain的執行,先執行Shiro中定義的Filter,最後再執行原FilterChian。可以打個比方,比如說本來有一條鐵鏈,一直螞蟻從鐵鏈的開端往末端爬,其中某一環叫ShiroFilter,那麼當螞蟻爬到ShiroFilter這一環時,將鐵鏈打斷,並且接上另一端鐵鏈(Shiro中自定義的Filter),這樣就構成了一條新的鐵鏈。然後螞蟻繼續爬行(後續的執行過程)。

最後附上預設註冊的filters:

public enum DefaultFilter {

    anon(AnonymousFilter.class),
    authc(FormAuthenticationFilter.class),
    authcBasic(BasicHttpAuthenticationFilter.class),
    logout(LogoutFilter.class),
    noSessionCreation(NoSessionCreationFilter.class),
    perms(PermissionsAuthorizationFilter.class),
    port(PortFilter.class),
    rest(HttpMethodPermissionFilter.class),
    roles(RolesAuthorizationFilter.class),
    ssl(SslFilter.class),
    user(UserFilter.class);
}

水平有限,寫得蠻不容易,看原始碼加寫花了整整2天。希望對大家能有幫助~