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

  • 2019 年 10 月 3 日
  • 筆記

本篇是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內置的一些過濾器,這些過濾器正好是在這裡初始化並添加到過濾器執行鏈中的,每個過濾器都有不同的功能,我們常用的其實只有前面兩個。

img

回到上上一步中,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中被實現了。

img

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

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

img

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

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

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