1. 程式人生 > >Shiro許可權管理框架(三):Shiro中許可權過濾器的初始化流程和實現原理

Shiro許可權管理框架(三):Shiro中許可權過濾器的初始化流程和實現原理

本篇是Shiro系列第三篇,Shiro中的過濾器初始化流程和實現原理。Shiro基於URL的許可權控制是通過Filter實現的,本篇從我們注入的ShiroFilterFactoryBean開始入手,翻看原始碼追尋Shiro中的過濾器的實現原理。


初始化流程

ShiroFilterFactoryBean實現了FactoryBean介面,那麼Spring在初始化的時候必然會呼叫ShiroFilterFactoryBean的getObject()獲取例項,而ShiroFilterFactoryBean也在此時做了一系列初始化操作。

關於FactoryBean的介紹和實現方式另外也記了一篇:https://www.guitu18.com/post/2019/04/28/33.html

在getObject()中會呼叫createInstance(),初始化相關的東西都在這裡了,程式碼貼過來去掉了註釋和校驗相關的程式碼。

    protected AbstractShiroFilter createInstance() throws Exception {
        SecurityManager securityManager = getSecurityManager();
        FilterChainManager manager = createFilterChainManager();
        PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
        chainResolver.setFilterChainManager(manager);
        return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
    }

這裡面首先獲取了我們在ShiroConfig中注入好引數的SecurityManager,再次強調,這位是Shiro中的核心元件。然後建立了一個FilterChainManager,這個類看名字就知道是用來管理和操作過濾器執行鏈的,我們來看它的建立方法createFilterChainManager()。

    protected FilterChainManager createFilterChainManager() {
        DefaultFilterChainManager manager = new DefaultFilterChainManager();
        Map<String, Filter> defaultFilters = manager.getFilters();
        for (Filter filter : defaultFilters.values()) {
            applyGlobalPropertiesIfNecessary(filter);
        }
        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);
                }
                manager.addFilter(name, filter, false);
            }
        }
        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;
    }

第一步new了一個DefaultFilterChainManager,在它的構造方法中將filters和filterChains兩個成員變數都初始化為一個能保持插入順序的LinkedHashMap了,之後再呼叫addDefaultFilters()新增Shiro內建的一些過濾器。

    public DefaultFilterChainManager() {
        this.filters = new LinkedHashMap<String, Filter>();
        this.filterChains = new LinkedHashMap<String, NamedFilterList>();
        addDefaultFilters(false);
    }
    protected void addDefaultFilters(boolean init) {
        for (DefaultFilter defaultFilter : DefaultFilter.values()) {
            addFilter(defaultFilter.name(), defaultFilter.newInstance(), init, false);
        }
    }

這裡用列舉列出了所有Shiro內建過濾器的例項。

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

以上程式碼有省略,上面列出的列舉型別正好對應Shiro第一篇提到的Shiro內建的一些過濾器,這些過濾器正好是在這裡初始化並新增到過濾器執行鏈中的,每個過濾器都有不同的功能,我們常用的其實只有前面兩個。

回到上上一步中,DefaultFilterChainManager初始化完成後,遍歷了每一個預設的過濾器並呼叫了applyGlobalPropertiesIfNecessary()設定一些必要的全域性屬性。

    private void applyGlobalPropertiesIfNecessary(Filter filter) {
        applyLoginUrlIfNecessary(filter);
        applySuccessUrlIfNecessary(filter);
        applyUnauthorizedUrlIfNecessary(filter);
    }

在這個方法中呼叫了三個方法,三個方法邏輯是一樣的,分別是設定loginUrl、successUrl和unauthorizedUrl,我們就看第一個applyLoginUrlIfNecessary()。

    private void applyLoginUrlIfNecessary(Filter filter) {
        String loginUrl = getLoginUrl();
        if (StringUtils.hasText(loginUrl) && (filter instanceof AccessControlFilter)) {
            AccessControlFilter acFilter = (AccessControlFilter) filter;
            String existingLoginUrl = acFilter.getLoginUrl();
            if (AccessControlFilter.DEFAULT_LOGIN_URL.equals(existingLoginUrl)) {
                acFilter.setLoginUrl(loginUrl);
            }
        }
    }

看方法名就知道是要設定loginUrl,如果我們配置了loginUrl,那麼會將AccessControlFilter中預設的loginUrl替換為我們設定的值,預設的loginUrl為/login.jsp。後面兩個方法道理一樣,都是將我們設定的引數替換進去,只不過第三個認證失敗跳轉URL的預設值為null。

繼續回到上一步,Map<String, Filter> filters = getFilters(); 這裡是獲取我們自定義的過濾器,預設是為空的,如果我們配置了自定義的過濾器,那麼會將其新增到filters中。至此filters中包含著Shiro內建的過濾器和我們配置的所有過濾器。

下一步,遍歷filterChainDefinitionMap,這個filterChainDefinitionMap就是我們在ShiroConfig中注入進去的攔截規則配置。這裡是根據我們配置的過濾器規則建立建立過濾器執行鏈。

    public void createChain(String chainName, String chainDefinition) {
        String[] filterTokens = splitChainDefinition(chainDefinition);
        for (String token : filterTokens) {
            String[] nameConfigPair = toNameConfigPair(token);
            addToChain(chainName, nameConfigPair[0], nameConfigPair[1]);
        }
    }

chainName是我們配置的過濾路徑,chainDefinition是該路徑對應的過濾器,通常我們都是一對一的配置,比如:filterMap.put("/login", "anon");,但看到這個方法我們知道了一個過濾路徑其實是可以通過傳入["filter1","filter2"...]配置多個過濾器的。在這裡會根據我們配置的過濾路徑和過濾器對映關係一步步配置過濾器執行鏈。

    public void addToChain(String chainName, String filterName, String chainSpecificFilterConfig) {
        Filter filter = getFilter(filterName);
        applyChainConfig(chainName, filter, chainSpecificFilterConfig);
        NamedFilterList chain = ensureChain(chainName);
        chain.add(filter);
    }

先從filters中根據filterName獲取對應過濾器,然後ensureChain()會先從filterChains根據chainName獲取NamedFilterList,獲取不到就建立一個並新增到filterChains然後返回。

    protected NamedFilterList ensureChain(String chainName) {
        NamedFilterList chain = getChain(chainName);
        if (chain == null) {
            chain = new SimpleNamedFilterList(chainName);
            this.filterChains.put(chainName, chain);
        }
        return chain;
    }

因為過濾路徑和過濾器是一對多的關係,所以ensureChain()返回的NamedFilterList其實就是一個有著name稱屬性的List<Filter>,這個name儲存的就是過濾路徑,List儲存著我們配置的過濾器。獲取到NamedFilterList後在將過濾器加入其中,這樣過濾路徑和過濾器對映關係就初始化好了。

至此,createInstance()中的createFilterChainManager()才算執行完成,它返回了一個FilterChainManager例項。之後再將這個FilterChainManager注入PathMatchingFilterChainResolver中,它是一個過濾器執行鏈解析器。

PathMatchingFilterChainResolver中的方法不多,最為重要的是這個getChain()方法。

    public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
        FilterChainManager filterChainManager = getFilterChainManager();
        if (!filterChainManager.hasChains()) {
            return null;
        }
        String requestURI = getPathWithinApplication(request);
        for (String pathPattern : filterChainManager.getChainNames()) {
            if (pathMatches(pathPattern, requestURI)) {
                return filterChainManager.proxy(originalChain, pathPattern);
            }
        }
        return null;
    }

看到形參中ServletRequest和ServletResponse這兩個引數是不是感覺特別親切,終於看到了點熟悉的東西了,一看就知道肯定跟請求有關。是的,我們每次請求伺服器都會呼叫這個方法,根據請求的URL去匹配過濾器執行鏈中的過濾路徑,匹配上了就返回其對應的過濾器進行過濾。

這個方法中的filterChainManager.getChainNames()返回的是根據我們的配置配置生成的執行鏈的過濾路徑集合,執行鏈生成的順序跟我們的配置的順序相同。從前文中我們也可以看到,在DefaultFilterChainManager的構造方法中將filterChains初始化為一個LinkedHashMap。所以在我的Shiro筆記第一篇中提到要將範圍大的過濾器放在後面就是這個道理,如果第一個匹配的過濾路徑就是/**那後面的過濾器永遠也匹配不上。


過濾實現原理

那麼這個getChain()是如何被呼叫的呢?既然是HTTP請求那肯定是從Tomcat過來的,當一個請求到達Tomcat時,Tomcat以責任鏈的形式呼叫了一系列Filter,OncePerRequestFilter就是眾多Filter中的一個。它所實現的doFilter()方法呼叫了自身的抽象方法doFilterInternal(),這個方法在它的子類AbstractShiroFilter中被實現了。

PathMatchingFilterChainResolver.getChain()就是被在doFilterInternal()中被一步步呼叫的呼叫的。

    protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, 
                                    final FilterChain chain) throws ServletException, IOException {
            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;
                }
            });
    }

這裡先獲獲取濾器,然後執行。

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

獲取過濾器方法如下。

    protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
        FilterChain chain = origChain;
        FilterChainResolver resolver = getFilterChainResolver();
        if (resolver == null) {
            return origChain;
        }
        FilterChain resolved = resolver.getChain(request, response, origChain);
        if (resolved != null) {
            chain = resolved;
        } else {
        }
        return chain;
    }

通過getFilterChainResolver()就拿到了上面提到的過濾器執行鏈解析器PathMatchingFilterChainResolver,然後再呼叫它的getChain()匹配獲取過濾器,最終過濾器在executeChain()中被執行。


首發地址:https://www.guitu18.com/post/2019/08/01/45.html

總結

Shiro框架在我們配置的ShiroFilterFactoryBean進行初始化的時候就做了很多初始化操作,將我們配置的過濾器規則一步步新增對應的過濾器到過濾器執行鏈中,這個執行鏈最終被放入執行鏈解析器。當有請求到達Tomcat時,通過Tomcat中的Filter責任鏈執行流程,最終Shiro所定義的AbstractShiroFilter.doFilter()被執行,那麼它會去獲取執行鏈解析器,通過解析器拿到執行鏈中的過濾器並執行,這樣就實現了基於URL的許可權過濾。

本文結束,這篇也算是淺入了Shiro原始碼瞭解了一下Shiro過濾器的初始化以及執行過程,相比Spring的原始碼Shiro的原始碼要簡單易懂的很多很多,它沒有Spring那麼繞。每次看原始碼的時候,我都有下面這種感覺,特別是看Spring原始碼的時候這種感覺尤為強烈:

對於框架我們所配置和呼叫的,永遠是浮在水面上的那一點點,不點進去,你永遠不知道下邊是一個怎樣的龐然大物。封裝的越好的框架,浮現出來的越少,隱藏的部分就越多。

比如SpringBoot,為什麼能通過一個main方法就啟動一個專案,web.xml呢,application.properties呢;SpringMVC為什麼就需要一個@RequestMapping就能實現從URL到方法的呼叫;Shiro為什麼僅需要@RequiresPermissions就能實現方法級別的許可權控制。

學到的越多就越是感覺自己知道的越少,這是一個很矛盾卻又真實存在的感覺。不說了該學習了,上面的最後一條方法級別許可權控制下一篇寫,時間待定,因為最近在專案中剛好遇到資料庫優化相關問題,想先去看看MySQL