­

shiro源碼解析-doFilter

  • 2019 年 10 月 30 日
  • 筆記

shiro應該算的上java中最流行的許可權框架了,使用的多了,便想著研究一下源碼,看它究竟怎麼運行的。

doFilter是shiro對於每個請求都會走的一個效驗過程。它的流程如下

DelegatingFilterProxy開始,執行dofilter(),這裡是一個代理模式,執行的是WebApplicationContext中的filter執行的dofilter方法,這個filter就是shiroFilter.從OncePerRequestFilter開始跟蹤,它的執行dofilter()。在dofilter()中,只有這個一個判斷,就是已經執行過shiro filter的請求不會再去執行shiro filter,實現方式是在http attribute中set一個屬性(filter類名.FILTERED),即shiroFilter.FILTERED。接著執行invokeDelegate調用代理對象的dofilter()方法

@Override  public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {        // Lazily initialize the delegate if necessary.      Filter delegateToUse = this.delegate;      if (delegateToUse == null) {      	synchronized (this.delegateMonitor) {      		delegateToUse = this.delegate;      		if (delegateToUse == null) {      			WebApplicationContext wac = findWebApplicationContext();      			if (wac == null) {      				throw new IllegalStateException("No WebApplicationContext found: " +      						"no ContextLoaderListener or DispatcherServlet registered?");      			}      			delegateToUse = initDelegate(wac);      		}  		    this.delegate = delegateToUse;      	}      }        // 調用代理對象的dofilter方法      invokeDelegate(delegateToUse, request, response, filterChain);  }
protected void invokeDelegate(        Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)        throws ServletException, IOException {     delegate.doFilter(request, response, filterChain);  }

delegatefilter的跟蹤從AbstractShiroFilter這個父類開始,它是OncePerRequestFilter的子類,OncePerRequestFilter會調用doFilterInternal抽象方法,AbstractShiroFilter去實現它。在doFilterInternal方法中,它會包裝request,response,接著將包裝後的request,response處理,使用的ServletRequestWrapperServletResponseWrapper,但是包裝對象還是原始的request和response,沒有替代。這麼做的原因應該是便於自定義擴展,比如做一些針對流的操作,流不可重複讀寫,而採用其他對象包裝後,可以把讀寫後的內容到另一個流里,再去返回。接著生成一個請求對應Subject,讓這個Subject去執行新執行緒的executeChain執行鏈。

為什麼不用執行緒池去管理執行緒,而要採用新建執行緒這種粗暴的方式執行filterChain鏈,難道Subject的數量是限制了? 在源碼中,執行緒並沒有做大小的限制,subject也沒有做數量大小的限制,來一個請求就新增一個subject。這裡我們採用的方式是重寫SubjectFactory,通過redis去快取principals,根據快取principals,判斷快取authenticated,通過上面的參數,new Subject。繼承Subject,加上user屬性,快取user。

executeChain()方法中,它會在根據請求url,在與FilterChainManager中所有的shiroFilter的chainName(定義的url名)比較,如果存在匹配,這返回對應的filterList。默認使用LinkedHashMap去存url與filterList。並將匹配filter包裝到一個FilterChain對象中。接著執行FilterChain

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 {          updateSessionLastAccessTime(request, response);          executeChain(request, response, chain);          return null;      }  });

ProxiedFilterChain是FilterChain的shiro實現類,用於filters鏈的實現,實現方式很巧妙,在chain執行一個filter,並自增下標,在filter.doFilter()中又調用chain.doFilter,實現鏈的接下來filter的調用。這個做法是不怎麼標準的「責任鏈模式」。

public class ProxiedFilterChain implements FilterChain {        private static final Logger log = LoggerFactory.getLogger(ProxiedFilterChain.class);      private FilterChain orig; //web.xml配置的filter      private List<Filter> filters; //shiro的filter      private int index = 0; //執行到了哪一個filter        public ProxiedFilterChain(FilterChain orig, List<Filter> filters) {          // init property      }        public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {          if (this.filters == null || this.filters.size() == this.index) {              this.orig.doFilter(request, response);          } else {              this.filters.get(this.index++).doFilter(request, response, this);          }      }  }

這次執行的就是匹配filter執行dofilter,我們這裡匹配的filter是AuthenticatingFilter下的子類。它會先走AdviceFilterdoFilterInternal()。在doFilterInternal(),依次調用preHandleexecuteChainpostHandle三個方法,就好像AOP的切面中的執行前,執行,執行後三個方法。preHandle就是這個filter驗證的邏輯,executeChain將調用chain.dofilter(),繼續執行下一個filter,postHandle()是遞歸回來後,去執行什麼操作,默認不執行任何操作。這樣執行鏈chain中所有filter,設計精巧,腦洞清奇。如果某個filter失效了,直接在preHandle()中用webTool.write寫返回,或默認跳轉頁面。

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

調用preHandle會先進入AccessControlFilter重寫的preHandle方法,它會判斷允許通過處理或禁止通過處理。用||判斷,所以isAccessAllowed通過了就不會再走onAccessDenied()

這裡有一個設計的bug,就是訪問login介面的時候帶正確token,它在isAccessAllowed方法中返回true,不會進入onAccessDenied。而生成並返回新token的處理一般就是在onAccessDenied中,它複雜未通過效驗的處理。 解決方法是,重寫isAccessAllowed方法,在方法中加上isLoginUrl()的判斷,如果是loginUrl,返回false,讓它走isAccessAllowed

public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {      return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);  }

isAccessAllowed方法

@Override  protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {      return super.isAccessAllowed(request, response, mappedValue) ||                  (!isLoginRequest(request, response) && isPermissive(mappedValue));  }

AuthorizationFilter的onAccessDenied方法

protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {        Subject subject = getSubject(request, response);      // If the subject isn't identified, redirect to login URL      if (subject.getPrincipal() == null) {          saveRequestAndRedirectToLogin(request, response);      } else {          // If subject is known but not authorized, redirect to the unauthorized URL if there is one          // If no unauthorized URL is specified, just return an unauthorized HTTP status code          String unauthorizedUrl = getUnauthorizedUrl();          //SHIRO-142 - ensure that redirect _or_ error code occurs - both cannot happen due to response commit:          if (StringUtils.hasText(unauthorizedUrl)) {              WebUtils.issueRedirect(request, response, unauthorizedUrl);          } else {              WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);          }      }      return false;  }

這是子AuthorizationFilter的onAccessDenied

Subject subject = getSubject(request, response);  // If the subject isn't identified, redirect to login URL  if (subject.getPrincipal() == null) {      saveRequestAndRedirectToLogin(request, response);  } else {      // If subject is known but not authorized, redirect to the unauthorized URL if there is one      // If no unauthorized URL is specified, just return an unauthorized HTTP status code      String unauthorizedUrl = getUnauthorizedUrl();      //SHIRO-142 - ensure that redirect _or_ error code occurs - both cannot happen due to response commit:      if (StringUtils.hasText(unauthorizedUrl)) {          WebUtils.issueRedirect(request, response, unauthorizedUrl);      } else {          WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);      }  }

實際上在我們項目中,只有一個filter執行,在preHandle()方法中驗證是否通過效驗,如果沒有通過,使用調用login介面,接著直接webTool.write,返回response,結束請求。

如果return ture,它接著執行filter,並由tomcat的filter WsFilter,進入程式介面。

dofilter流程圖

注意 spring的filter源碼也是這麼處理的,從OncePerRequestFilter開始,做法一致。

總體來說,shiro的dofilter流程如下。首先進入shiro的主filter,這個filter的作用是獲得該url與shiro適配的filters,並把這個適配的List<filter>放入filterChain中去繼續執行。接著執行filteChain中的下一個filter,這個filter是FilterChain中的,它是shiro中篩選url的filter。這些filter都繼承AdviceFilter,去執行doFilterInternal()doFilterInternal()依次執行preHandleexecuteChainpostHandle三個方法,分別是判斷是否執行(具體filter的業務就是重寫preHandle,),執行鏈中的下一個filter,filter執行結束後的操作。AdviceFilter有的executeChain會調用下一個filter,從而實現所有filter的效驗。AccessControlFilter