(原)
shiro最核心的2個操作,一個是登錄的實現,一就是過濾器了。登錄有時間再補錄說明,這里分析下shiro過濾器怎樣玩的。
1、目標
這里會按如下順序逐一看其實原理,並盡量找出其出處。
先看一下shiro過濾器有哪些及它們的別名分別對應哪些類:點這里
這里只分析平時用的最多的一個:authc過濾器,對應的處理器的類是FormAuthenticationFilter。
2、繼承關系及結構
authc: 1.AbstractFilter > 2.NameableFilter > 3.OncePerRequestFilter > 4.AdviceFilter > 5.PathMatchingFilter > 6.AccessControlFilter > 7.AuthenticationFilter > 8.AuthenticatingFilter > 9.FormAuthenticationFilter
根據這個結構關系可以清楚的知道,接下來要分析的FormAuthenticationFilter上面還有8個。
3、Filter過濾器簡介
tomcat中的自定義過濾器必需實現Filter接口,過濾器的實現方法為Filter中的
doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
如果一個請求被該過濾器攔截,想要到達用戶最后請求的地址,那么一定得有:filterChain.doFilter(request, response);這個方法的調用,表示通過過濾,請求可以到達用戶的請求地址。否則用戶的請求地址不會被調用,用戶的請求被過濾器攔截。
4、shiro過濾器分析
1、AbstractFilter
位於shiro過濾器最頂層,是個抽象方法,直接繼承了Filter,
它只做了一些其本的初始化操作,並沒有實現Filter中的doFilter方法。
總結:AbstractFilter只做了一些基本的初始化,並沒有過多的邏輯在里面。
2、NameableFilter
允許通過getName和setName方式給過濾器命名。如果沒有給該過濾器命名,那么默認將會使用web.xml中給定的名稱(FilterConfiger的filterName)
該類可以理解為為過濾器命名用的,也沒有實現Filter中的doFilter方法。
總結:NameableFilter是用於給過濾器命名使用的。
3、OncePerRequestFilter
filter的基類(說白了,就是正真實現了Filter中的doFilter方法的類),它用來保證在每個servlet容器上,第個請求都只會被過濾一次,不會重復過濾。
通過getAlreadyFilteredAttributeName()方法來鑒別請求是否已經被過濾過。默認的實現基於具體過濾的實例名稱。
接下來看看具體邏輯:
1、 判斷過濾器是否已經執行過,如果執行過,調用filterChain.doFilter(request, response)直接放行,不再重復執行該過濾器的處理邏輯,直接走下一個過濾器或者通過該過濾進入到實際請求方法中。
2、 判斷過濾器是否開啟,該類有個成員變量eabled,默認為true(注釋給的解釋是大多數的過濾器都是希望開啟的,所以默認值為true),過濾器為開啟狀態,如果該過濾器沒有被開啟中,也同上面的邏輯一樣,直接走下一個過濾器或者通過該過濾進入到實際請求方法中。
3、這里又分3步,
1、 設置一個已經執行過濾器的屬性名稱在request中。
2、 調用doFilterInternal方法,執行真正的過濾器處理邏輯。
3、 這個過濾器處理完后,將過濾器的屬性名稱從request中移出。這樣如果程序執行到第2步,過濾又被調用了,它將會走到第一個if中,直接略過這個過濾器的處理,這樣就保證了,每個過濾器在處理一個請求的時候只會被執行一次。
總結:OncePerRequestFilter提供了一個實際處理過濾業務的方法doFilterInternal,並且保證每個請求只會被該過濾器過濾一次。
4、AdviceFilter
這個過濾器,類似於開啟了AOP環繞通知,提供了preHandle,postHandle和afterCompletion這三個方法。
其過濾邏輯為:doFilterInternal方法。
executeChain方法中的實現為:chain.doFilter(request, response);
所以如果preHandle方法返回false,則說明過濾器不會執行chain.doFilter,意味着請求被攔截掉了,不會進入到用戶請求的地址上去。如果為true,表示過濾器放行了過濾的邏輯通過。
然后會執行postHandle方法(該類中的postHandle方法為空實現),
最后會執行一個,這個方法用於對一些異常進行處理(目前也是空實現)。
通過以上可以看出,過濾的邏輯代碼實際上是在preHandle這個方法中處理的,這個方法的返回值true和false決定了請求放行和請求被攔截。
總結:AdviceFilter提供了類似於AOP環繞通知式的編程方式,其處理攔截的邏輯是在preHandle方法中完成的。preHandle方法返回true和false代表了通過過濾,請求可以到達用戶的請求地址和過濾器攔截掉了用戶的請求。
5、PathMatchingFilter
這個過濾器會處理指定的請求路徑,和對其它路徑的請求放行。
打個比方:如果配置如下攔截,/hello=authc,
這就意味着,請用戶請求/hello時,這時的authc過濾就會對這個請求攔截並進行過濾邏輯處理。如果一個用戶請求是/world,則不會對此請求過濾。
這里有2個處理邏輯,
1、判斷:appliedPaths為空,則放行,不作攔截請求的處理。
這里的appliedPaths是一個地址過濾器的映射map。
key存放的是請求的URL,value是對應該URL處理的Filter過濾器,這個 value也可以是空值。
2、如果用戶的請求與配置中攔截的請求匹配,則會調用isFilterChainContinued方法進行下一步處理。
如果攔截器是開啟的,則調用方法onPreHandle進行攔截處理。
onPreHandle方法默認返回true,也不對請求進行攔截處理,所以如果需要有自定義處理攔截的邏輯,需要由子類覆蓋這個方法來完成。
總結:PathMatchingFilter對配置了url請求攔截的地址進行過濾器過濾,對沒有配置攔截的url請求直接放行,不進行攔截。如果請求需要過濾,則處理過濾的邏輯由子類實現onPreHandle完成。
6、AccessControlFilter
如果用戶沒有認證(即登錄),那么這個過濾器就是控制訪問資源和用戶重定向到登錄頁面的過濾器的父類。當一個用戶沒有認證(即登錄)時,可以通過saveRequestAndRedirectToLogin這個方法,重定向到登錄頁面。
以上是該類的文檔說明,實際還有一些其它的邏輯在里面,接着5、PathMatchingFilter的思路走,我們得看看onPreHandle中的實現。
其中調用了二個方法的邏輯或,isAccessAllowed和onAccessDenied。
isAccessAllowed如果請求允許正常處理,返回true。否則返回false由方法onAccessDenied進行處理請求。即isAccessAllowed返回true表示用戶已登錄過,false表示用戶還未登錄。
onAccessDenied即登錄驗證在isAccessAllowed方法中被拒絕以后調用,其中參數mappedValue可以通過配置獲取到,也可以為null。這個方法的委托方法isAccessAllowed(request, response)處理,只有2個參數,大多數被isAccessAllowed方法拒絕后的行為(即isAccessAllowed返回false),再做處理時不需要mappedValue這個配置參數。
總結:AccessControlFilter中的onPreHandle處理真正的攔截邏輯,isAccessAllowed方法驗證用戶是否登錄,onAccessDenied處理用戶沒登錄后的邏輯,在這個過濾器中並沒有給出isAccessAllowed和onAccessDenied方法的實現,下一步得去子類中看,目前只能通過文檔的注釋去了解這些方法大概會執行什么樣的操作。
7、AuthenticationFilter
需要對當前用戶認證的過濾器的基類,這個類封裝了一些檢查用戶是否在系統中已經驗證過的邏輯,子類需要對未驗證的請求執行特定的邏輯。
AuthenticationFilter這個類的子類為AuthenticatingFilter,通過第6個過濾器可以知道,最后處理登錄認驗的實際上就是isAccessAllowed和onAccessDenied這二個方法。再根據文檔的描述,這個類實現了isAccessAllowed的邏輯,而它的子類AuthenticatingFilter實現了onAccessDenied的邏輯。
先看isAccessAllowed方法:
先取出當前用戶,然后再返回當前用戶是否已認證。
通過以上邏輯可以看到,如果用戶已認證(即用戶已登錄),那么用戶的請戶的將會往后執行,不再進行過濾攔截,如果用戶沒有登錄,那么將會執行后面onAccessDenied方法中的內容。而onAccessDenied是在其子類中實現的。
總結:AuthenticationFilter實現了isAccessAllowed方法,如果用戶已登錄,那么過濾器將直接放行,如果用戶沒有登錄,那么再由其子類中的onAccessDenied方法處理后續邏輯。
8、AuthenticatingFilter
該類會嘗試基於用戶的請求,自動的去執行一些身份認證。
先看下這個過濾器所有的方法。
該類並沒有實現onAccessDenied方法。而是提供了一些登錄,創建用戶,和登錄成功或失敗后的重定向跳轉等方法。
以下是登錄:
如果登錄成功執行onLoginSuccess,失敗則執行onLoginFailure。
總結:AuthenticatingFilter提供了一些如登錄,和重定向跳轉的方法,並沒有onAccessDenied方法的實現,那么這個實現就只能在最后一個子類FormAuthenticationFilter中完成了。
9、FormAuthenticationFilter
要求請求用戶進行身份認證,以便使請求繼續,如果沒有認證,則強制讓該請求重定向到你配置中的登錄URL(loginUrl)。
這個構造器會構造一個UsernamePasswordToken對象,里面包含了username, password,和rememberMe這三個請求參數。當調用Subject.login(usernamePasswordToken)方法時,它會嘗試自動的執行登錄操作,要注意的是,這個嘗試登錄的操作僅僅只會在isLoginSubmission(request,response)返回true且是一個POST請求的登錄操作。
如果嘗試登錄失敗,則會將AuthenticationException異常寫入到request的屬性當中,這個屬性的key是是failureKeyAttribute(這是個變量的名字,其值為shiroLoginFailure),FQCN能用作i18n的key或查找機制,向用戶解釋為什么會登錄失敗。(也就是說,如果被攔截,可以在request. getAttribute(“shiroLoginFailure”)中得到返回的錯誤消息)。
如果你想用你自己的代碼處理身份認證和登錄,可以用使用PassThruAuthenticationFilter,它允許loginUrl的請求直接傳遞到你自己的容器代碼中去。
這個文檔說明比較長,我們這里主要看onAccessDenied的實現。
1、 isLoginRequest是否是登錄請求,如果不是登錄請求,直接通過saveRequestAndRedirectToLogin方法返回到登錄頁面(這里的登錄請求指的是loginUrl,它有一個默認地址是/login.jsp)。
saveRequestAndRedirectToLogin這個方法在AccessControlFilter中實現。
2、如果是登錄請求,則通過isLoginSubmission判斷是否是http的post請求。如果不是則返回false,說明用戶請求的是登錄頁面的get請求,用戶就是直接通過瀏覽器訪問登錄頁面的,這時只需要返回true,放行就可以了。如果isLoginSubmission返回true,表明用戶是一個http的post請求,並且是訪問登錄的url請求。
3、如果用戶是一個http的post請求,那么就執行executeLogin(request, response)方法做登錄操作。
首先會從createToken中把請求參數封裝了token(可以理解為一個登錄對象),這個token中包含了用戶名密碼。
其中用戶名,密碼是一個固定的變量值username,password。
即頁請求傳過來的參數默認是username和password。它會根據這2個參數封裝成authenticationToken對象。然后根據這個對象進行登錄操作。(一般這個操作都是會在我們自定義的realm中完成,這里就暫不介紹登錄)
登錄成功,直接執行onLoginSuccess(token, subject, request, response)方法。
登錄成功執行issueSuccessRedirect方法重定向到登錄成功的頁面,然后返回false(這里返回false是因為用戶是請求的登錄操作,然后被authc過濾器給攔截掉並且登錄成功了,就沒必需繼續再往后面走了。),注意:這里的onLoginSuccess方法是FormAuthenticationFilter中的,不是AuthenticatingFilter中的。
登錄失敗則執行onLoginFailure方法,這個方向會在request中的屬性中存入失敗原因,key值為:shiroLoginFailure,並且最終返回true,意味着用戶的登錄請求最后到達到我們的后台,只是在過濾器中已經做過一次登錄了,並且是登錄失敗,所以我們自己的登錄地址在編碼時不需要再重復做登錄認證操作,只需要從request中取出shiroLoginFailure認證報錯信息,做相應的邏輯處理就可以了。
總結:FormAuthenticationFilter實現了認證失敗后的處理邏輯,即用戶在未登錄情況下處理的后續操作,如果用戶是一個非登錄請求,那么會直接重定向到登錄頁面(即配置中的loginUrl頁面),如果是一個登錄請求,且是GET請頁,那么直接放行登錄請求,如果是POST請求,則會從請求中獲取默認的username,password去嘗試登錄,如果登錄成功,則由過濾器直接重定向到登錄成功的url上去,並攔截掉用戶的請求。如果登錄失敗,則由過濾器直接重定向到登錄失敗的url上去,並將失敗信息寫入到request中去,key值為shiroLoginFailure,然后過濾器放行,直接到達用戶請求的地址,我們就可以在這個請求地址中取到登錄失敗的數據做相應的操作了。
最后梳理一下全部流程中方法的調用。
首先過濾器會調用doFilter(在OncePerRequestFilter中)方法,然后再調用doFilterInternal方法(在AdviceFilter中),然后再調用preHandle、executeChain、postHandle(在AdviceFilter中)這3個方法,實際攔截業務在preHandle方法中,然后再調用onPreHandle(在AccessControlFilter中)方法,然后再調用isAccessAllowed(在AuthenticationFilter中)方法和onAccessDenied(在FormAuthenticationFilter中)方法。