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); }
delegate
filter的跟蹤從AbstractShiroFilter
這個父類開始,它是OncePerRequestFilter
的子類,OncePerRequestFilter
會調用doFilterInternal
抽象方法,AbstractShiroFilter
去實現它。在doFilterInternal
方法中,它會包裝request,response,接著將包裝後的request,response處理,使用的ServletRequestWrapper
,ServletResponseWrapper
,但是包裝對象還是原始的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
下的子類。它會先走AdviceFilter
的doFilterInternal()
。在doFilterInternal()
,依次調用preHandle
,executeChain
,postHandle
三個方法,就好像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()
依次執行preHandle
,executeChain
,postHandle
三個方法,分別是判斷是否執行(具體filter的業務就是重寫preHandle
,),執行鏈中的下一個filter,filter執行結束後的操作。AdviceFilter
有的executeChain會調用下一個filter,從而實現所有filter的效驗。AccessControlFilter