1. 程式人生 > >第八章------攔截器機制

第八章------攔截器機制

8.1 攔截器介紹

Shiro使用了與Servlet一樣的Filter介面進行擴充套件;所以如果對Filter不熟悉可以參考《Servlet3.1規範》http://www.iteye.com/blogs/subjects/Servlet-3-1瞭解Filter的工作原理。首先下圖是Shiro攔截器的基礎類圖:

1、NameableFilter

NameableFilter給Filter起個名字,如果沒有設定預設就是FilterName;還記得之前的如authc嗎?當我們組裝攔截器鏈時會根據這個名字找到相應的攔截器例項;

2、OncePerRequestFilter

OncePerRequestFilter用於防止多次執行Filter的;也就是說一次請求只會走一次攔截器鏈;另外提供enabled屬性,表示是否開啟該攔截器例項,預設enabled=true表示開啟,如果不想讓某個攔截器工作,可以設定為false即可。

3、ShiroFilter

ShiroFilter是整個Shiro的入口點,用於攔截需要安全控制的請求進行處理,這個之前已經用過了。

4、AdviceFilter

AdviceFilter提供了AOP風格的支援,類似於SpringMVC中的Interceptor:

Java程式碼  收藏程式碼
  1. boolean preHandle(ServletRequest request, ServletResponse response) throws Exception  
  2. void postHandle(ServletRequest request, ServletResponse response) throws
     Exception  
  3. void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception;   

preHandler:類似於AOP中的前置增強;在攔截器鏈執行之前執行;如果返回true則繼續攔截器鏈;否則中斷後續的攔截器鏈的執行直接返回;進行預處理(如基於表單的身份驗證、授權)

postHandle:類似於AOP中的後置返回增強;在攔截器鏈執行完成後執行;進行後處理(如記錄執行時間之類的);

afterCompletion:類似於AOP中的後置最終增強;即不管有沒有異常都會執行;可以進行清理資源(如接觸Subject與執行緒的繫結之類的);

5、PathMatchingFilter

PathMatchingFilter提供了基於Ant風格的請求路徑匹配功能及攔截器引數解析的功能,如“roles[admin,user]”自動根據“,”分割解析到一個路徑引數配置並繫結到相應的路徑:

Java程式碼  收藏程式碼
  1. boolean pathsMatch(String path, ServletRequest request)  
  2. boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception   

pathsMatch:該方法用於path與請求路徑進行匹配的方法;如果匹配返回true;

onPreHandle:在preHandle中,當pathsMatch匹配一個路徑後,會呼叫opPreHandler方法並將路徑繫結引數配置傳給mappedValue;然後可以在這個方法中進行一些驗證(如角色授權),如果驗證失敗可以返回false中斷流程;預設返回true;也就是說子類可以只實現onPreHandle即可,無須實現preHandle。如果沒有path與請求路徑匹配,預設是通過的(即preHandle返回true)。

6、AccessControlFilter

AccessControlFilter提供了訪問控制的基礎功能;比如是否允許訪問/當訪問拒絕時如何處理等:

Java程式碼  收藏程式碼
  1. abstract boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception;  
  2. boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception;  
  3. abstract boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception;   

isAccessAllowed:表示是否允許訪問;mappedValue就是[urls]配置中攔截器引數部分,如果允許訪問返回true,否則false;

onAccessDenied:表示當訪問拒絕時是否已經處理了;如果返回true表示需要繼續處理;如果返回false表示該攔截器例項已經處理了,將直接返回即可。

onPreHandle會自動呼叫這兩個方法決定是否繼續處理:

Java程式碼  收藏程式碼
  1. boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {  
  2.     return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);  
  3. }   

另外AccessControlFilter還提供瞭如下方法用於處理如登入成功後/重定向到上一個請求: 

Java程式碼  收藏程式碼
  1. void setLoginUrl(String loginUrl) //身份驗證時使用,預設/login.jsp  
  2. String getLoginUrl()  
  3. Subject getSubject(ServletRequest request, ServletResponse response) //獲取Subject例項  
  4. boolean isLoginRequest(ServletRequest request, ServletResponse response)//當前請求是否是登入請求  
  5. void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException //將當前請求儲存起來並重定向到登入頁面  
  6. void saveRequest(ServletRequest request) //將請求儲存起來,如登入成功後再重定向回該請求  
  7. void redirectToLogin(ServletRequest request, ServletResponse response) //重定向到登入頁面   

比如基於表單的身份驗證就需要使用這些功能。

到此基本的攔截器就完事了,如果我們想進行訪問訪問的控制就可以繼承AccessControlFilter;如果我們要新增一些通用資料我們可以直接繼承PathMatchingFilter。

8.2 攔截器鏈

Shiro對Servlet容器的FilterChain進行了代理,即ShiroFilter在繼續Servlet容器的Filter鏈的執行之前,通過ProxiedFilterChain對Servlet容器的FilterChain進行了代理;即先走Shiro自己的Filter體系,然後才會委託給Servlet容器的FilterChain進行Servlet容器級別的Filter鏈執行;Shiro的ProxiedFilterChain執行流程:1、先執行Shiro自己的Filter鏈;2、再執行Servlet容器的Filter鏈(即原始的Filter)。

而ProxiedFilterChain是通過FilterChainResolver根據配置檔案中[urls]部分是否與請求的URL是否匹配解析得到的。 

Java程式碼  收藏程式碼
  1. FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain);  

即傳入原始的chain得到一個代理的chain。

Shiro內部提供了一個路徑匹配的FilterChainResolver實現:PathMatchingFilterChainResolver,其根據[urls]中配置的url模式(預設Ant風格)=攔截器鏈和請求的url是否匹配來解析得到配置的攔截器鏈的;而PathMatchingFilterChainResolver內部通過FilterChainManager維護著攔截器鏈,比如DefaultFilterChainManager實現維護著url模式與攔截器鏈的關係。因此我們可以通過FilterChainManager進行動態動態增加url模式與攔截器鏈的關係。

DefaultFilterChainManager會預設新增org.apache.shiro.web.filter.mgt.DefaultFilter中宣告的攔截器:

Java程式碼  收藏程式碼
  1. public enum DefaultFilter {  
  2.     anon(AnonymousFilter.class),  
  3.     authc(FormAuthenticationFilter.class),  
  4.     authcBasic(BasicHttpAuthenticationFilter.class),  
  5.     logout(LogoutFilter.class),  
  6.     noSessionCreation(NoSessionCreationFilter.class),  
  7.     perms(PermissionsAuthorizationFilter.class),  
  8.     port(PortFilter.class),  
  9.     rest(HttpMethodPermissionFilter.class),  
  10.     roles(RolesAuthorizationFilter.class),  
  11.     ssl(SslFilter.class),  
  12.     user(UserFilter.class);  
  13. }   

下一節會介紹這些攔截器的作用。

如果要註冊自定義攔截器,IniSecurityManagerFactory/WebIniSecurityManagerFactory在啟動時會自動掃描ini配置檔案中的[filters]/[main]部分並註冊這些攔截器到DefaultFilterChainManager;且建立相應的url模式與其攔截器關係鏈。如果使用Spring後續章節會介紹如果註冊自定義攔截器。

如果想自定義FilterChainResolver,可以通過實現WebEnvironment介面完成:

Java程式碼  收藏程式碼
  1. public class MyIniWebEnvironment extends IniWebEnvironment {  
  2.     @Override  
  3.     protected FilterChainResolver createFilterChainResolver() {  
  4.         //在此處擴充套件自己的FilterChainResolver  
  5.         return super.createFilterChainResolver();  
  6.     }  
  7. }   

FilterChain之間的關係。如果想動態實現url-攔截器的註冊,就可以通過實現此處的FilterChainResolver來完成,比如:

Java程式碼  收藏程式碼
  1. //1、建立FilterChainResolver  
  2. PathMatchingFilterChainResolver filterChainResolver =  
  3.         new PathMatchingFilterChainResolver();  
  4. //2、建立FilterChainManager  
  5. DefaultFilterChainManager filterChainManager = new DefaultFilterChainManager();  
  6. //3、註冊Filter  
  7. for(DefaultFilter filter : DefaultFilter.values()) {  
  8.     filterChainManager.addFilter(  
  9.         filter.name(), (Filter) ClassUtils.newInstance(filter.getFilterClass()));  
  10. }  
  11. //4、註冊URL-Filter的對映關係  
  12. filterChainManager.addToChain("/login.jsp""authc");  
  13. filterChainManager.addToChain("/unauthorized.jsp""anon");  
  14. filterChainManager.addToChain("/**""authc");  
  15. filterChainManager.addToChain("/**""roles""admin");  
  16. //5、設定Filter的屬性  
  17. FormAuthenticationFilter authcFilter =  
  18.          (FormAuthenticationFilter)filterChainManager.getFilter("authc");  
  19. authcFilter.setLoginUrl("/login.jsp");  
  20. RolesAuthorizationFilter rolesFilter =  
  21.           (RolesAuthorizationFilter)filterChainManager.getFilter("roles");  
  22. rolesFilter.setUnauthorizedUrl("/unauthorized.jsp");  
  23. filterChainResolver.setFilterChainManager(filterChainManager);  
  24. return filterChainResolver;   

此處自己去實現註冊filter,及url模式與filter之間的對映關係。可以通過定製FilterChainResolver或FilterChainManager來完成諸如動態URL匹配的實現。

然後再web.xml中進行如下配置Environment:  

Java程式碼  收藏程式碼
  1. <context-param>  
  2. <param-name>shiroEnvironmentClass</param-name> <param-value>com.github.zhangkaitao.shiro.chapter8.web.env.MyIniWebEnvironment</param-value>  
  3. </context-param>   

8.3 自定義攔截器

通過自定義自己的攔截器可以擴充套件一些功能,諸如動態url-角色/許可權訪問控制的實現、根據Subject身份資訊獲取使用者資訊繫結到Request(即設定通用資料)、驗證碼驗證、線上使用者資訊的儲存等等,因為其本質就是一個Filter;所以Filter能做的它就能做。

對於Filter的介紹請參考《Servlet規範》中的Filter部分:

1、擴充套件OncePerRequestFilter

OncePerRequestFilter保證一次請求只調用一次doFilterInternal,即如內部的forward不會再多執行一次doFilterInternal: 

Java程式碼  收藏程式碼
  1. public class MyOncePerRequestFilter extends OncePerRequestFilter {  
  2.     @Override  
  3.     protected void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {  
  4.         System.out.println("=========once per request filter");  
  5.         chain.doFilter(request, response);  
  6.     }  
  7. }   

然後再shiro.ini配置檔案中:

Java程式碼  收藏程式碼
  1. [main]  
  2. myFilter1=com.github.zhangkaitao.shiro.chapter8.web.filter.MyOncePerRequestFilter  
  3. #[filters]  
  4. #myFilter1=com.github.zhangkaitao.shiro.chapter8.web.filter.MyOncePerRequestFilter  
  5. [urls]  
  6. /**=myFilter1   

Filter可以在[main]或[filters]部分註冊,然後在[urls]部分配置url與filter的對映關係即可。

2、擴充套件AdviceFilter

AdviceFilter提供了AOP的功能,其實現和SpringMVC中的Interceptor思想一樣:具體可參考我的SpringMVC教程中的處理器攔截器部分:

Java程式碼  收藏程式碼
  1. public class MyAdviceFilter extends AdviceFilter {  
  2.     @Override  
  3.     protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {  
  4.         System.out.println("====預處理/前置處理");  
  5.         return true;//返回false將中斷後續攔截器鏈的執行  
  6.     }  
  7.     @Override  
  8.     protected void postHandle(ServletRequest request, ServletResponse response) throws Exception {  
  9.         System.out.println("====後處理/後置返回處理");  
  10.     }  
  11.     @Override  
  12.     public void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception {  
  13.         System.out.println("====完成處理/後置最終處理");  
  14.     }  
  15. }   

preHandle:進行請求的預處理,然後根據返回值決定是否繼續處理(true:繼續過濾器鏈);可以通過它實現許可權控制;

postHandle:執行完攔截器鏈之後正常返回後執行;

afterCompletion:不管最後有沒有異常,afterCompletion都會執行,完成如清理資源功能。

然後在shiro.ini中進行如下配置: 

Java程式碼  收藏程式碼
  1. [filters]  
  2. myFilter1=com.github.zhangkaitao.shiro.chapter8.web.filter.MyOncePerRequestFilter  
  3. myFilter2=com.github.zhangkaitao.shiro.chapter8.web.filter.MyAdviceFilter  
  4. [urls]  
  5. /**=myFilter1,myFilter2   

該過濾器的具體使用可參考我的SpringMVC教程中的處理器攔截器部分。

3、PathMatchingFilter

PathMatchingFilter繼承了AdviceFilter,提供了url模式過濾的功能,如果需要對指定的請求進行處理,可以擴充套件PathMatchingFilter: 

Java程式碼  收藏程式碼
  1. public class MyPathMatchingFilter extends PathMatchingFilter {  
  2.     @Override  
  3.     protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {  
  4.        System.out.println("url matches,config is " + Arrays.toString((String[])mappedValue));  
  5.        return true;  
  6.     }  
  7. }   

preHandle:會進行url模式與請求url進行匹配,如果匹配會呼叫onPreHandle;如果沒有配置url模式/沒有url模式匹配,預設直接返回true;

onPreHandle:如果url模式與請求url匹配,那麼會執行onPreHandle,並把該攔截器配置的引數傳入。預設什麼不處理直接返回true。

然後在shiro.ini中進行如下配置:

Java程式碼  收藏程式碼
  1. [filters]  
  2. myFilter3=com.github.zhangkaitao.shiro.chapter8.web.filter.MyPathMatchingFilter  
  3. [urls]  
  4. /**= myFilter3[config]   

/**就是註冊給PathMatchingFilter的url模式,config就是攔截器的配置引數,多個之間逗號分隔,onPreHandle使用mappedValue接收引數值。

4、擴充套件AccessControlFilter

AccessControlFilter繼承了PathMatchingFilter,並擴充套件了了兩個方法:

Java程式碼  收藏程式碼
  1. public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {  
  2.     return isAccessAllowed(request, response, mappedValue)  
  3.      || onAccessDenied(request, response, mappedValue);  
  4. }   

isAccessAllowed:即是否允許訪問,返回true表示允許;

onAccessDenied:表示訪問拒絕時是否自己處理,如果返回true表示自己不處理且繼續攔截器鏈執行,返回false表示自己已經處理了(比如重定向到另一個頁面)。

Java程式碼  收藏程式碼
  1. public class MyAccessControlFilter extends AccessControlFilter {  
  2.     protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {  
  3.         System.out.println("access allowed");  
  4.         return true;  
  5.     }  
  6.     protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {  
  7.         System.out.println("訪問拒絕也不自己處理,繼續攔截器鏈的執行");  
  8.         return true;  
  9.     }  
  10. }   

然後在shiro.ini中進行如下配置:

Java程式碼  收藏程式碼
  1. [filters]  
  2. myFilter4=com.github.zhangkaitao.shiro.chapter8.web.filter.MyAccessControlFilter  
  3. [urls]  
  4. /**=myFilter4  

5、基於表單登入攔截器 

之前我們已經使用過Shiro內建的基於表單登入的攔截器了,此處自己做一個類似的基於表單登入的攔截器。

Java程式碼  收藏程式碼
  1. public class FormLoginFilter extends PathMatchingFilter {  
  2.     private String loginUrl = "/login.jsp";  
  3.     private String successUrl = "/";  
  4.     @Override  
  5.     protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {  
  6.         if(SecurityUtils.getSubject().isAuthenticated()) {  
  7.             return true;//已經登入過  
  8.         }  
  9.         HttpServletRequest req = (HttpServletRequest) request;  
  10.         HttpServletResponse resp = (HttpServletResponse) response;  
  11.         if(isLoginRequest(req)) {  
  12.             if("post".equalsIgnoreCase(req.getMethod())) {//form表單提交  
  13.                 boolean loginSuccess = login(req); //登入  
  14.                 if(loginSuccess) {  
  15.                     return redirectToSuccessUrl(req, resp);  
  16.                 }  
  17.             }  
  18.             return true;//繼續過濾器鏈  
  19.         } else {//儲存當前地址並重定向到登入介面  
  20.             saveRequestAndRedirectToLogin(req, resp);  
  21.             return false;  
  22.         }  
  23.     }  
  24.     private boolean redirectToSuccessUrl(HttpServletRequest req, HttpServletResponse resp) throws IOException {  
  25.         WebUtils.redirectToSavedRequest(req, resp, successUrl);  
  26.         return false;  
  27.     }  
  28.     private void saveRequestAndRedirectToLogin(HttpServletRequest req, HttpServletResponse resp) throws IOException {  
  29.         WebUtils.saveRequest(req);  
  30.         WebUtils.issueRedirect(req, resp, loginUrl);  
  31.     }  
  32.     private boolean login(HttpServletRequest req) {  
  33.         String username = req.getParameter("username");  
  34.         String password = req.getParameter("password");  
  35.         try {  
  36.             SecurityUtils.getSubject().login(new UsernamePasswordToken(username, password));  
  37.         } catch (Exception e) {  
  38.             req.setAttribute("shiroLoginFailure", e.getClass());  
  39.             return false;  
  40.         }  
  41.         return true;  
  42.     }  
  43.     private boolean isLoginRequest(HttpServletRequest req) {  
  44.         return pathsMatch(loginUrl, WebUtils.getPathWithinApplication(req));  
  45.     }  
  46. }   

onPreHandle主要流程:

1、首先判斷是否已經登入過了,如果已經登入過了繼續攔截器鏈即可;

2、如果沒有登入,看看是否是登入請求,如果是get方法的登入頁面請求,則繼續攔截器鏈(到請求頁面),否則如果是get方法的其他頁面請求則儲存當前請求並重定向到登入頁面;

3、如果是post方法的登入頁面表單提交請求,則收集使用者名稱/密碼登入即可,如果失敗了儲存錯誤訊息到“shiroLoginFailure”並返回到登入頁面;

4、如果登入成功了,且之前有儲存的請求,則重定向到之前的這個請求,否則到預設的成功頁面。

shiro.ini配置

Java程式碼  收藏程式碼
  1. [filters]  
  2. formLogin=com.github.zhangkaitao.shiro.chapter8.web.filter.FormLoginFilter  
  3. [urls]  
  4. /test.jsp=formLogin  
  5. /login.jsp=formLogin   

啟動伺服器輸入http://localhost:8080/chapter8/test.jsp測試時,會自動跳轉到登入頁面,登入成功後又會跳回到test.jsp頁面。

此處可以通過繼承AuthenticatingFilter實現,其提供了很多登入相關的基礎程式碼。另外可以參考Shiro內嵌的FormAuthenticationFilter的原始碼,思路是一樣的。

6、任意角色授權攔截器

Shiro提供roles攔截器,其驗證使用者擁有所有角色,沒有提供驗證使用者擁有任意角色的攔截器。

Java程式碼  收藏程式碼
  1. public class AnyRolesFilter extends AccessControlFilter {  
  2.     private String unauthorizedUrl = "/unauthorized.jsp";  
  3.     private String loginUrl = "/login.jsp";  
  4.     protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {  
  5.         String[] roles = (String[])mappedValue;  
  6.         if(roles == null) {  
  7.             return true;//如果沒有設定角色引數,預設成功  
  8.         }  
  9.         for(String role : roles) {  
  10.             if(getSubject(request, response).hasRole(role)) {  
  11.                 return true;  
  12.             }  
  13.         }  
  14.         return false;//跳到onAccessDenied處理  
  15.     }  
  16.     @Override  
  17.     protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {  
  18.         Subject subject = getSubject(request, response);  
  19.         if (subject.getPrincipal() == null) {//表示沒有登入,重定向到登入頁面  
  20.             saveRequest(request);  
  21.             WebUtils.issueRedirect(request, response, loginUrl);  
  22.         } else {  
  23.             if (StringUtils.hasText(unauthorizedUrl)) {//如果有未授權頁面跳轉過去  
  24.                 WebUtils.issueRedirect(request, response, unauthorizedUrl);  
  25.             } else {//否則返回401未授權狀態碼  
  26.                 WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);  
  27.             }  
  28.         }  
  29.         return false;  
  30.     }  
  31. }   

流程:

1、首先判斷使用者有沒有任意角色,如果沒有返回false,將到onAccessDenied進行處理;

2、如果使用者沒有角色,接著判斷使用者有沒有登入,如果沒有登入先重定向到登入;

3、如果使用者沒有角色且設定了未授權頁面(unauthorizedUrl),那麼重定向到未授權頁面;否則直接返回401未授權錯誤碼。

shiro.ini配置

Java程式碼  收藏程式碼
  1. [filters]  
  2. anyRoles=com.github.zhangkaitao.shiro.chapter8.web.filter.AnyRolesFilter  
  3. [urls]  
  4. /test.jsp=formLogin,anyRoles[admin,user]  
  5. /login.jsp=formLogin   

此處可以繼承AuthorizationFilter實現,其提供了授權相關的基礎程式碼。另外可以參考Shiro內嵌的RolesAuthorizationFilter的原始碼,只是實現hasAllRoles邏輯。

8.4 預設攔截器

Shiro內建了很多預設的攔截器,比如身份驗證、授權等相關的。預設攔截器可以參考org.apache.shiro.web.filter.mgt.DefaultFilter中的列舉攔截器:  

預設攔截器名