權限框架通常都是一堆過濾器、攔截器的組合運用,在shiro中,有多少個內置的過濾器你知道嗎?在哪些場景用那些過濾器,這篇文章但願你能對shiro有個新的認識!javascript
別忘了,點個 [在看] 支持一下哈~html
前兩篇原創shiro相關文章:前端
二、只須要6個步驟,springboot集成shiro,並完成登陸git
咱們都知道shiro是個認證權限框架,除了登陸、退出邏輯咱們須要侵入項目代碼以外,驗證用戶是否已經登陸、是否擁有權限的代碼其實都是過濾器來完成的,能夠這麼說,shiro其實就是一個過濾器鏈集合。web
那麼今天咱們詳細討論一下shiro底層到底給咱們提供了多少默認的過濾器供咱們使用,又都有什麼用呢?帶着問題,咱們先去shiro官網看看對於默認過濾器集的說明。面試
When running a web-app, Shiro will create some useful default Filter instances and make them available in the [main] section automatically. You can configure them in main as you would any other bean and reference them in your chain definitions.The default Filter instances available automatically are defined by the DefaultFilter enum and the enum’s name field is the name available for configuration.ajax
翻譯過來意思:spring
當運行web應用程序時,Shiro將建立一些有用的默認過濾器實例,並使它們在[main]部分自動可用。您能夠像配置任何其餘bean同樣在main中配置它們,並在鏈定義中引用它們。apache
默認篩選器實例由DefaultFilter enum中定義,enum s name字段是可用於配置的名稱。
因而我看了一下DefaultFilter
的源碼:
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); ... }
終於知道咱們經常使用的anon、authc、perms、roles、user過濾器是哪裏來的了!這些過濾器咱們都是能夠直接使用的。但你要弄清楚這些默認過濾器,你還不得不去深刻了解一下shiro更底層爲咱們提供的過濾器,基本咱們的這些默認過濾器都是經過繼承這幾個底層過濾器演變而來的。
那麼這些過濾器都有哪些呢?咱們來看一個圖:
上面我標記了7個咱們接下來要介紹的過濾器,咱們一個個來介紹,弄清楚這些過濾器以後,相信你對shiro的認識會更深一層了。具體authc、perms、roles等這些默認過濾器與這7個過濾器有什麼關係你就會明白。
這個過濾器還得說說,shiro最底層的抽象過濾器,雖然咱們極少直接繼承它,它經過實現Filter
得到過濾器的特性。
完成一些過濾器基本初始化操做,FilterConfig
:過濾器配置對象,用於servlet容器在初始化期間將信息傳遞給其餘過濾器。
命名過濾器,給過濾器定義名稱!也是比較基層的過濾器了,未拓展其餘功能,咱們不多會直接繼承這個過濾器。爲重寫doFilter方法。
重寫doFilter方法,保證每一個servlet方法只會被過濾一次。能夠看到doFilter方法中,第一行代碼就是String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
而後經過request.getAttribute(alreadyFilteredAttributeName) != null
來判斷過濾器是否已經被調用過,從而保證過濾器不會被重複調用。
進入方法以前,先標記alreadyFilteredAttributeName
爲True,抽象doFilterInternal
方法執行以後再remove掉alreadyFilteredAttributeName
。
因此OncePerRequestFilter過濾器保證只會被一次調用的功能,提供了抽象方法doFilterInternal
讓後面的過濾器能夠重寫,執行真正的過濾器處理邏輯。
protected abstract void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException;
這個過濾器咱們已經能夠開始在咱們的項目繼承使用,好比攔截用戶請求,判斷用戶是否已經登陸(攜帶token或cookie信息),若是未登陸則返回Json數據告知未登陸!
好比:
開源mblog博客項目中,過濾器就是繼承OncePerRequestFilter。
/** * 公衆號:MarkerHub **/ public class AuthenticatedFilter extends OncePerRequestFilter { // 前端彈窗的js代碼 private static final String JS = "<script type='text/javascript'>var wp=window.parent; if(wp!=null){while(wp.parent&&wp.parent!==wp){wp=wp.parent;}wp.location.href='%1$s';}else{window.location.href='%1$s';}</script>"; private String loginUrl = "/login"; // 重寫doFilterInternal方法 @Override protected void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { Subject subject = SecurityUtils.getSubject(); // 已經登錄就跳過過濾器 if (subject.isAuthenticated() || subject.isRemembered()) { chain.doFilter(request, response); } else { // 未登陸就返回json或者js代碼 WebUtils.saveRequest(request); String path = WebUtils.getContextPath((HttpServletRequest) request); String url = loginUrl; if (StringUtils.isNotBlank(path) && path.length() > 1) { url = path + url; } if (isAjaxRequest((HttpServletRequest) request)) { response.setContentType("application/json;charset=UTF-8"); response.getWriter().print(JSON.toJSONString(Result.failure("您尚未登陸!"))); } else { response.getWriter().write(new Formatter().format(JS, url).toString()); } } } }
未登陸狀況,ajax請求過濾器返回您尚未登陸!
提示,web請求則返回一段js代碼,前端渲染會跳出一個登錄窗口,這也就是未什麼你們常遇到的點擊登陸,當前跳出一個登錄彈窗的一種實現方式!
效果:
看到Advice,很天然想到切面環繞編程,通常有pre、post、after幾個方法。因此這個AdviceFilter過濾器就是提供了和AOP類似的切面功能。
繼承OncePerRequestFilter過濾器重寫doFilterInternal方法,咱們能夠先看看:
能夠看到上面4個序號:
因而,咱們從OncePerRequestFilter的一個doFilterInternal分化成了切面編程,更容易先後控制執行邏輯。因此若是繼承AdviceFilter時候,咱們能夠重寫preHandle方法,判斷用戶是否知足已登陸或者其餘業務邏輯,返回false時候表示不經過過濾器。
請求路徑匹配過濾器,經過匹配請求url,判斷請求是否須要過濾,若是url未在須要過濾的集合內,則跳過,不然進入isFilterChainContinued
的onPreHandle方法。
咱們能夠看下代碼:
從上面3個步驟中能夠看到,PathMatchingFilter提供的功能是:自定義匹配url,匹配上的請求最終跳轉到onPreHandle
方法。
這個過濾器爲後面的經常使用過濾器提供的基礎,好比咱們在config中配置以下
/login = anon /admin/* = authc
攔截/login請求,通過AnonymousFilter過濾器,咱們能夠看下
public class AnonymousFilter extends PathMatchingFilter { /** * 公衆號:MarkerHub **/ @Override protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) { // Always return true since we allow access to anyone return true; } }
AnonymousFilter重寫了onPreHandle方法,只不過直接返回了true,說明攔截的連接能夠直接經過,不須要其餘攔截邏輯。
而authc->FormAuthenticationFilter也是間接繼承了PathMatchingFilter。
public class FormAuthenticationFilter extends AuthenticatingFilter
因此,須要攔截某個連接進行業務邏輯過濾的能夠繼承PathMatchingFilter方法拓展哈。
訪問控制過濾器。繼承PathMatchingFilter過濾器,重寫onPreHandle方法,又分出了兩個抽象方法來控制
因此,咱們如今能夠經過重寫這個抽象兩個方法來控制過濾邏輯。另外多提供了3個方法,方便後面的過濾器使用。
/** * 公衆號:MarkerHub **/ protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException { saveRequest(request); redirectToLogin(request, response); } protected void saveRequest(ServletRequest request) { WebUtils.saveRequest(request); } protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException { String loginUrl = getLoginUrl(); WebUtils.issueRedirect(request, response, loginUrl); }
其中redirectToLogin提供了調整到登陸頁面的邏輯與實現,爲後面的過濾器發現未登陸跳轉到登陸頁面提供了基礎。
這個過濾器,咱們能夠靈活運用。
繼承AccessControlFilter,重寫了isAccessAllowed方法,經過判斷用戶是否已經完成登陸來判斷用戶是否容許繼續後面的邏輯判斷。這裏能夠看出,從這個過濾器開始,後續的判斷會與用戶的登陸狀態相關,直接繼承這些過濾器,咱們不須要再本身手動去判斷用戶是否已經登陸。而且提供了登陸成功以後跳轉的方法。
public abstract class AuthenticationFilter extends AccessControlFilter { public void setSuccessUrl(String successUrl) { this.successUrl = successUrl; } protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { Subject subject = getSubject(request, response); return subject.isAuthenticated(); } }
繼承AuthenticationFilter,提供了自動登陸、是否登陸請求等方法。
/** * 公衆號:MarkerHub **/ public abstract class AuthenticatingFilter extends AuthenticationFilter { public static final String PERMISSIVE = "permissive"; //TODO - complete JavaDoc protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { AuthenticationToken token = createToken(request, response); if (token == null) { String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " + "must be created in order to execute a login attempt."; throw new IllegalStateException(msg); } try { Subject subject = getSubject(request, response); subject.login(token); return onLoginSuccess(token, subject, request, response); } catch (AuthenticationException e) { return onLoginFailure(token, e, request, response); } } protected abstract AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception; /** * 公衆號:MarkerHub **/ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { return super.isAccessAllowed(request, response, mappedValue) || (!isLoginRequest(request, response) && isPermissive(mappedValue)); } ... }
這個方法提供了自動登陸的課程,好比咱們獲取到token以後實行自動登陸,這場景仍是很場景的。
好比在開源項目renren-fast中,就是這樣處理的:
public class OAuth2Filter extends AuthenticatingFilter { @Override protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception { //獲取請求token String token = getRequestToken((HttpServletRequest) request); if(StringUtils.isBlank(token)){ return null; } return new OAuth2Token(token); } @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { if(((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())){ return true; } return false; } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { //獲取請求token,若是token不存在,直接返回401 String token = getRequestToken((HttpServletRequest) request); if(StringUtils.isBlank(token)){ HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setHeader("Access-Control-Allow-Credentials", "true"); httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin()); String json = new Gson().toJson(R.error(HttpStatus.SC_UNAUTHORIZED, "invalid token")); httpResponse.getWriter().print(json); return false; } return executeLogin(request, response); } /** *公衆號:MarkerHub **/ @Override protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setContentType("application/json;charset=utf-8"); httpResponse.setHeader("Access-Control-Allow-Credentials", "true"); httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin()); try { //處理登陸失敗的異常 Throwable throwable = e.getCause() == null ? e : e.getCause(); R r = R.error(HttpStatus.SC_UNAUTHORIZED, throwable.getMessage()); String json = new Gson().toJson(r); httpResponse.getWriter().print(json); } catch (IOException e1) { } return false; } /** * 獲取請求的token */ private String getRequestToken(HttpServletRequest httpRequest){ //從header中獲取token String token = httpRequest.getHeader("token"); //若是header中不存在token,則從參數中獲取token if(StringUtils.isBlank(token)){ token = httpRequest.getParameter("token"); } return token; } }
在onAccessDenied
方法校驗經過以後執行executeLogin
方法完成自動登陸!
基於form表單的帳號密碼自動登陸的過濾器,咱們只須要看這個方法就明白,和renren-fast的實現類似:
public class FormAuthenticationFilter extends AuthenticatingFilter { public static final String DEFAULT_USERNAME_PARAM = "username"; public static final String DEFAULT_PASSWORD_PARAM = "password"; public static final String DEFAULT_REMEMBER_ME_PARAM = "rememberMe"; protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) { String username = getUsername(request); String password = getPassword(request); return createToken(username, password, request, response); } /** * 公衆號:MarkerHub **/ protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { if (isLoginRequest(request, response)) { if (isLoginSubmission(request, response)) { if (log.isTraceEnabled()) { log.trace("Login submission detected. Attempting to execute login."); } return executeLogin(request, response); } else { if (log.isTraceEnabled()) { log.trace("Login page view."); } //allow them to see the login page ;) return true; } } else { if (log.isTraceEnabled()) { log.trace("Attempting to access a path which requires authentication. Forwarding to the " + "Authentication url [" + getLoginUrl() + "]"); } saveRequestAndRedirectToLogin(request, response); return false; } } }
onAccessDenied調用executeLogin方法。默認的token是UsernamepasswordToken。
好了,今天先到這裏啦,講了多好內置的過濾器,代碼有點多,大家能夠用電腦打開文章,而後仔細研究,並回想本身使用shiro過濾器的時候,是否是和我講的場景同樣,結合起來。
這裏是MarkerHub,我是呂一明,感謝關注與支持!後續更新請關注公衆號:MarkerHub
推薦閱讀:
分享一套SpringBoot開發博客系統源碼,以及完整開發文檔!速度保存!