shiro中攔截器機制
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代碼- boolean preHandle(ServletRequest request, ServletResponse response) throws Exception
- void postHandle(ServletRequest request, ServletResponse response) throws Exception
- 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代碼- boolean pathsMatch(String path, ServletRequest request)
- 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代碼- abstract boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception;
- boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception;
- abstract boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception;
isAccessAllowed:表示是否允許訪問;mappedValue就是[urls]配置中攔截器參數部分,如果允許訪問返回true,否則false;
onAccessDenied:表示當訪問拒絕時是否已經處理了;如果返回true表示需要繼續處理;如果返回false表示該攔截器實例已經處理了,將直接返回即可。
onPreHandle會自動調用這兩個方法決定是否繼續處理:
Java代碼- boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
- return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
- }
另外AccessControlFilter還提供了如下方法用於處理如登錄成功後/重定向到上一個請求:
Java代碼- void setLoginUrl(String loginUrl) //身份驗證時使用,默認/login.jsp
- String getLoginUrl()
- Subject getSubject(ServletRequest request, ServletResponse response) //獲取Subject實例
- boolean isLoginRequest(ServletRequest request, ServletResponse response)//當前請求是否是登錄請求
- void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException //將當前請求保存起來並重定向到登錄頁面
- void saveRequest(ServletRequest request) //將請求保存起來,如登錄成功後再重定向回該請求
- 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代碼- 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代碼- 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);
- }
下一節會介紹這些攔截器的作用。
如果要註冊自定義攔截器,IniSecurityManagerFactory/WebIniSecurityManagerFactory在啟動時會自動掃描ini配置文件中的[filters]/[main]部分並註冊這些攔截器到DefaultFilterChainManager;且創建相應的url模式與其攔截器關系鏈。如果使用Spring後續章節會介紹如果註冊自定義攔截器。
如果想自定義FilterChainResolver,可以通過實現WebEnvironment接口完成:
Java代碼- public class MyIniWebEnvironment extends IniWebEnvironment {
- @Override
- protected FilterChainResolver createFilterChainResolver() {
- //在此處擴展自己的FilterChainResolver
- return super.createFilterChainResolver();
- }
- }
FilterChain之間的關系。如果想動態實現url-攔截器的註冊,就可以通過實現此處的FilterChainResolver來完成,比如:
Java代碼- //1、創建FilterChainResolver
- PathMatchingFilterChainResolver filterChainResolver =
- new PathMatchingFilterChainResolver();
- //2、創建FilterChainManager
- DefaultFilterChainManager filterChainManager = new DefaultFilterChainManager();
- //3、註冊Filter
- for(DefaultFilter filter : DefaultFilter.values()) {
- filterChainManager.addFilter(
- filter.name(), (Filter) ClassUtils.newInstance(filter.getFilterClass()));
- }
- //4、註冊URL-Filter的映射關系
- filterChainManager.addToChain("/login.jsp", "authc");
- filterChainManager.addToChain("/unauthorized.jsp", "anon");
- filterChainManager.addToChain("/**", "authc");
- filterChainManager.addToChain("/**", "roles", "admin");
- //5、設置Filter的屬性
- FormAuthenticationFilter authcFilter =
- (FormAuthenticationFilter)filterChainManager.getFilter("authc");
- authcFilter.setLoginUrl("/login.jsp");
- RolesAuthorizationFilter rolesFilter =
- (RolesAuthorizationFilter)filterChainManager.getFilter("roles");
- rolesFilter.setUnauthorizedUrl("/unauthorized.jsp");
- filterChainResolver.setFilterChainManager(filterChainManager);
- return filterChainResolver;
此處自己去實現註冊filter,及url模式與filter之間的映射關系。可以通過定制FilterChainResolver或FilterChainManager來完成諸如動態URL匹配的實現。
然後再web.xml中進行如下配置Environment:
Java代碼- <context-param>
- <param-name>shiroEnvironmentClass</param-name> <param-value>com.github.zhangkaitao.shiro.chapter8.web.env.MyIniWebEnvironment</param-value>
- </context-param>
8.3 自定義攔截器
通過自定義自己的攔截器可以擴展一些功能,諸如動態url-角色/權限訪問控制的實現、根據Subject身份信息獲取用戶信息綁定到Request(即設置通用數據)、驗證碼驗證、在線用戶信息的保存等等,因為其本質就是一個Filter;所以Filter能做的它就能做。
對於Filter的介紹請參考《Servlet規範》中的Filter部分:
http://www.iteye.com/blogs/subjects/Servlet-3-1。
1、擴展OncePerRequestFilter
OncePerRequestFilter保證一次請求只調用一次doFilterInternal,即如內部的forward不會再多執行一次doFilterInternal:
Java代碼- public class MyOncePerRequestFilter extends OncePerRequestFilter {
- @Override
- protected void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
- System.out.println("=========once per request filter");
- chain.doFilter(request, response);
- }
- }
然後再shiro.ini配置文件中:
Java代碼- [main]
- myFilter1=com.github.zhangkaitao.shiro.chapter8.web.filter.MyOncePerRequestFilter
- #[filters]
- #myFilter1=com.github.zhangkaitao.shiro.chapter8.web.filter.MyOncePerRequestFilter
- [urls]
- /**=myFilter1
Filter可以在[main]或[filters]部分註冊,然後在[urls]部分配置url與filter的映射關系即可。
2、擴展AdviceFilter
AdviceFilter提供了AOP的功能,其實現和SpringMVC中的Interceptor思想一樣:具體可參考我的SpringMVC教程中的處理器攔截器部分:
http://www.iteye.com/blogs/subjects/kaitao-springmvc
Java代碼- public class MyAdviceFilter extends AdviceFilter {
- @Override
- protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
- System.out.println("====預處理/前置處理");
- return true;//返回false將中斷後續攔截器鏈的執行
- }
- @Override
- protected void postHandle(ServletRequest request, ServletResponse response) throws Exception {
- System.out.println("====後處理/後置返回處理");
- }
- @Override
- public void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception {
- System.out.println("====完成處理/後置最終處理");
- }
- }
preHandle:進行請求的預處理,然後根據返回值決定是否繼續處理(true:繼續過濾器鏈);可以通過它實現權限控制;
postHandle:執行完攔截器鏈之後正常返回後執行;
afterCompletion:不管最後有沒有異常,afterCompletion都會執行,完成如清理資源功能。
然後在shiro.ini中進行如下配置:
Java代碼- [filters]
- myFilter1=com.github.zhangkaitao.shiro.chapter8.web.filter.MyOncePerRequestFilter
- myFilter2=com.github.zhangkaitao.shiro.chapter8.web.filter.MyAdviceFilter
- [urls]
- /**=myFilter1,myFilter2
該過濾器的具體使用可參考我的SpringMVC教程中的處理器攔截器部分。
3、PathMatchingFilter
PathMatchingFilter繼承了AdviceFilter,提供了url模式過濾的功能,如果需要對指定的請求進行處理,可以擴展PathMatchingFilter:
Java代碼- public class MyPathMatchingFilter extends PathMatchingFilter {
- @Override
- protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
- System.out.println("url matches,config is " + Arrays.toString((String[])mappedValue));
- return true;
- }
- }
preHandle:會進行url模式與請求url進行匹配,如果匹配會調用onPreHandle;如果沒有配置url模式/沒有url模式匹配,默認直接返回true;
onPreHandle:如果url模式與請求url匹配,那麽會執行onPreHandle,並把該攔截器配置的參數傳入。默認什麽不處理直接返回true。
然後在shiro.ini中進行如下配置:
Java代碼- [filters]
- myFilter3=com.github.zhangkaitao.shiro.chapter8.web.filter.MyPathMatchingFilter
- [urls]
- /**= myFilter3[config]
/**就是註冊給PathMatchingFilter的url模式,config就是攔截器的配置參數,多個之間逗號分隔,onPreHandle使用mappedValue接收參數值。
4、擴展AccessControlFilter
AccessControlFilter繼承了PathMatchingFilter,並擴展了了兩個方法:
Java代碼- public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
- return isAccessAllowed(request, response, mappedValue)
- || onAccessDenied(request, response, mappedValue);
- }
isAccessAllowed:即是否允許訪問,返回true表示允許;
onAccessDenied:表示訪問拒絕時是否自己處理,如果返回true表示自己不處理且繼續攔截器鏈執行,返回false表示自己已經處理了(比如重定向到另一個頁面)。
Java代碼
- public class MyAccessControlFilter extends AccessControlFilter {
- protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
- System.out.println("access allowed");
- return true;
- }
- protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
- System.out.println("訪問拒絕也不自己處理,繼續攔截器鏈的執行");
- return true;
- }
- }
然後在shiro.ini中進行如下配置:
Java代碼- [filters]
- myFilter4=com.github.zhangkaitao.shiro.chapter8.web.filter.MyAccessControlFilter
- [urls]
- /**=myFilter4
5、基於表單登錄攔截器
之前我們已經使用過Shiro內置的基於表單登錄的攔截器了,此處自己做一個類似的基於表單登錄的攔截器。
Java代碼- public class FormLoginFilter extends PathMatchingFilter {
- private String loginUrl = "/login.jsp";
- private String successUrl = "/";
- @Override
- protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
- if(SecurityUtils.getSubject().isAuthenticated()) {
- return true;//已經登錄過
- }
- HttpServletRequest req = (HttpServletRequest) request;
- HttpServletResponse resp = (HttpServletResponse) response;
- if(isLoginRequest(req)) {
- if("post".equalsIgnoreCase(req.getMethod())) {//form表單提交
- boolean loginSuccess = login(req); //登錄
- if(loginSuccess) {
- return redirectToSuccessUrl(req, resp);
- }
- }
- return true;//繼續過濾器鏈
- } else {//保存當前地址並重定向到登錄界面
- saveRequestAndRedirectToLogin(req, resp);
- return false;
- }
- }
- private boolean redirectToSuccessUrl(HttpServletRequest req, HttpServletResponse resp) throws IOException {
- WebUtils.redirectToSavedRequest(req, resp, successUrl);
- return false;
- }
- private void saveRequestAndRedirectToLogin(HttpServletRequest req, HttpServletResponse resp) throws IOException {
- WebUtils.saveRequest(req);
- WebUtils.issueRedirect(req, resp, loginUrl);
- }
- private boolean login(HttpServletRequest req) {
- String username = req.getParameter("username");
- String password = req.getParameter("password");
- try {
- SecurityUtils.getSubject().login(new UsernamePasswordToken(username, password));
- } catch (Exception e) {
- req.setAttribute("shiroLoginFailure", e.getClass());
- return false;
- }
- return true;
- }
- private boolean isLoginRequest(HttpServletRequest req) {
- return pathsMatch(loginUrl, WebUtils.getPathWithinApplication(req));
- }
- }
onPreHandle主要流程:
1、首先判斷是否已經登錄過了,如果已經登錄過了繼續攔截器鏈即可;
2、如果沒有登錄,看看是否是登錄請求,如果是get方法的登錄頁面請求,則繼續攔截器鏈(到請求頁面),否則如果是get方法的其他頁面請求則保存當前請求並重定向到登錄頁面;
3、如果是post方法的登錄頁面表單提交請求,則收集用戶名/密碼登錄即可,如果失敗了保存錯誤消息到“shiroLoginFailure”並返回到登錄頁面;
4、如果登錄成功了,且之前有保存的請求,則重定向到之前的這個請求,否則到默認的成功頁面。
shiro.ini配置
Java代碼- [filters]
- formLogin=com.github.zhangkaitao.shiro.chapter8.web.filter.FormLoginFilter
- [urls]
- /test.jsp=formLogin
- /login.jsp=formLogin
啟動服務器輸入http://localhost:8080/chapter8/test.jsp測試時,會自動跳轉到登錄頁面,登錄成功後又會跳回到test.jsp頁面。
此處可以通過繼承AuthenticatingFilter實現,其提供了很多登錄相關的基礎代碼。另外可以參考Shiro內嵌的FormAuthenticationFilter的源碼,思路是一樣的。
6、任意角色授權攔截器
Shiro提供roles攔截器,其驗證用戶擁有所有角色,沒有提供驗證用戶擁有任意角色的攔截器。
Java代碼- public class AnyRolesFilter extends AccessControlFilter {
- private String unauthorizedUrl = "/unauthorized.jsp";
- private String loginUrl = "/login.jsp";
- protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
- String[] roles = (String[])mappedValue;
- if(roles == null) {
- return true;//如果沒有設置角色參數,默認成功
- }
- for(String role : roles) {
- if(getSubject(request, response).hasRole(role)) {
- return true;
- }
- }
- return false;//跳到onAccessDenied處理
- }
- @Override
- protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
- Subject subject = getSubject(request, response);
- if (subject.getPrincipal() == null) {//表示沒有登錄,重定向到登錄頁面
- saveRequest(request);
- WebUtils.issueRedirect(request, response, loginUrl);
- } else {
- if (StringUtils.hasText(unauthorizedUrl)) {//如果有未授權頁面跳轉過去
- WebUtils.issueRedirect(request, response, unauthorizedUrl);
- } else {//否則返回401未授權狀態碼
- WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
- }
- }
- return false;
- }
- }
流程:
1、首先判斷用戶有沒有任意角色,如果沒有返回false,將到onAccessDenied進行處理;
2、如果用戶沒有角色,接著判斷用戶有沒有登錄,如果沒有登錄先重定向到登錄;
3、如果用戶沒有角色且設置了未授權頁面(unauthorizedUrl),那麽重定向到未授權頁面;否則直接返回401未授權錯誤碼。
shiro.ini配置
Java代碼- [filters]
- anyRoles=com.github.zhangkaitao.shiro.chapter8.web.filter.AnyRolesFilter
- [urls]
- /test.jsp=formLogin,anyRoles[admin,user]
- /login.jsp=formLogin
此處可以繼承AuthorizationFilter實現,其提供了授權相關的基礎代碼。另外可以參考Shiro內嵌的RolesAuthorizationFilter的源碼,只是實現hasAllRoles邏輯。
8.4 默認攔截器
Shiro內置了很多默認的攔截器,比如身份驗證、授權等相關的。默認攔截器可以參考org.apache.shiro.web.filter.mgt.DefaultFilter中的枚舉攔截器:
默認攔截器名 |
攔截器類 |
說明(括號裏的表示默認值) |
身份驗證相關的 |
|
|
authc |
org.apache.shiro.web.filter.authc .FormAuthenticationFilter |
基於表單的攔截器;如“/**=authc”,如果沒有登錄會跳到相應的登錄頁面登錄;主要屬性:usernameParam:表單提交的用戶名參數名( username); passwordParam:表單提交的密碼參數名(password); rememberMeParam:表單提交的密碼參數名(rememberMe); loginUrl:登錄頁面地址(/login.jsp);successUrl:登錄成功後的默認重定向地址; failureKeyAttribute:登錄失敗後錯誤信息存儲key(shiroLoginFailure); |
authcBasic |
org.apache.shiro.web.filter.authc .BasicHttpAuthenticationFilter |
Basic HTTP身份驗證攔截器,主要屬性: applicationName:彈出登錄框顯示的信息(application); |
logout |
org.apache.shiro.web.filter.authc .LogoutFilter |
退出攔截器,主要屬性:redirectUrl:退出成功後重定向的地址(/);示例“/logout=logout” |
user |
org.apache.shiro.web.filter.authc .UserFilter |
用戶攔截器,用戶已經身份驗證/記住我登錄的都可;示例“/**=user” |
anon |
org.apache.shiro.web.filter.authc .AnonymousFilter |
匿名攔截器,即不需要登錄即可訪問;一般用於靜態資源過濾;示例“/static/**=anon” |
授權相關的 |
|
|
roles |
org.apache.shiro.web.filter.authz .RolesAuthorizationFilter |
角色授權攔截器,驗證用戶是否擁有所有角色;主要屬性: loginUrl:登錄頁面地址(/login.jsp);unauthorizedUrl:未授權後重定向的地址;示例“/admin/**=roles[admin]” |
perms |
org.apache.shiro.web.filter.authz .PermissionsAuthorizationFilter |
權限授權攔截器,驗證用戶是否擁有所有權限;屬性和roles一樣;示例“/user/**=perms["user:create"]” |
port |
org.apache.shiro.web.filter.authz .PortFilter |
端口攔截器,主要屬性:port(80):可以通過的端口;示例“/test= port[80]”,如果用戶訪問該頁面是非80,將自動將請求端口改為80並重定向到該80端口,其他路徑/參數等都一樣 |
rest |
org.apache.shiro.web.filter.authz .HttpMethodPermissionFilter |
rest風格攔截器,自動根據請求方法構建權限字符串(GET=read, POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)構建權限字符串;示例“/users=rest[user]”,會自動拼出“user:read,user:create,user:update,user:delete”權限字符串進行權限匹配(所有都得匹配,isPermittedAll); |
ssl |
org.apache.shiro.web.filter.authz .SslFilter |
SSL攔截器,只有請求協議是https才能通過;否則自動跳轉會https端口(443);其他和port攔截器一樣; |
其他 |
|
|
noSessionCreation |
org.apache.shiro.web.filter.session .NoSessionCreationFilter |
不創建會話攔截器,調用 subject.getSession(false)不會有什麽問題,但是如果 subject.getSession(true)將拋出 DisabledSessionException異常; |
另外還提供了一個org.apache.shiro.web.filter.authz.HostFilter,即主機攔截器,比如其提供了屬性:authorizedIps:已授權的ip地址,deniedIps:表示拒絕的ip地址;不過目前還沒有完全實現,不可用。
這些默認的攔截器會自動註冊,可以直接在ini配置文件中通過“攔截器名.屬性”設置其屬性:
Java代碼- perms.unauthorizedUrl=/unauthorized
另外如果某個攔截器不想使用了可以直接通過如下配置直接禁用:
Java代碼- perms.enabled=false
示例源代碼:https://github.com/zhangkaitao/shiro-example;
shiro中攔截器機制