上一篇已經分析了shiro的入口filter是SpringShiroFilter, 那么它的doFilter在哪兒呢?
我們看到它的直接父類AbstractShrioFilter繼承了OncePerRequestFilter類,該類是shiro內置的大部分filter的父類(抽像公共部分),在該類中定義了doFilter方法
OncePerRequestFilte
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//當前過濾器的名字 String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
//如果該過濾器執行過,那么將不執行同一個名字的過濾器 直接執行過濾鏈中的下一個過濾器 if ( request.getAttribute(alreadyFilteredAttributeName) != null ) { filterChain.doFilter(request, response);
} else if (!isEnabled(request, response) || shouldNotFilter(request) ) {
//如果當前過濾器設置了enabled屬性為false,則不執行,直接執行過濾鏈中的下一個過濾器 filterChain.doFilter(request, response); } else { //標志當前過濾器已經執行過 request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE); try {
//1、 核心方法 doFilterInternal(request, response, filterChain); } finally { //過濾鏈執行完畢后,清空request中的過濾鏈執行記錄 request.removeAttribute(alreadyFilteredAttributeName); } } }
由於我們是通過SpringShiroFilter攔截進來的那么會調用AbstractShrioFilter中的doFilterInternal
AbstractShrioFilter
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain) throws ServletException, IOException { //封裝容器的request和response為shiro自己的 其中在request中標識了當前不為servlet容器的session (在創建session時會用到servlet容器調用getSession()時 ) final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain); final ServletResponse response = prepareServletResponse(request, servletResponse, chain); //2、 創建subject(可以看出每次請求都會創建一個Subject對象) final Subject subject = createSubject(request, response); //執行過濾鏈 subject.execute(new Callable() { public Object call() throws Exception { updateSessionLastAccessTime(request, response); //修改session的最后活動時間 executeChain(request, response, chain); //執行過濾鏈 return null; } }); } //創建subject對象 protected WebSubject createSubject(ServletRequest request, ServletResponse response) { return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject(); } //7、執行過濾鏈 protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain) throws IOException, ServletException { //獲取當前請求對應的過濾鏈 FilterChain chain = getExecutionChain(request, response, origChain); chain.doFilter(request, response); }
WebSubject
//3、
public Builder(SecurityManager securityManager, ServletRequest request, ServletResponse response) { //每次都創建subject上下文(subjectContext), 並設置securityManager對象 super(securityManager); //將request和response添加到subject上下文(subjectContext), 即上面創建的對象 setRequest(request); setResponse(response); } //4、創建 public WebSubject buildWebSubject() { Subject subject = super.buildSubject(); return (WebSubject) subject; }
Subject
//5、調用DefaultSecurityManager的createSubject方法
public Subject buildSubject() {
return this.securityManager.createSubject(this.subjectContext); }
DefaultSecurityManager
// 6
public Subject createSubject(SubjectContext subjectContext) { //web的subjectContext時,會重新創建一個新的,其他的(ini等),只是copy SubjectContext context = copy(subjectContext); //驗證是否subject上下文中有securityMangary對象,如果沒有創建一個 context = ensureSecurityManager(context); //將session放入subjectContext 該session會從cookie或者rul上帶的JSESSIONID(默認) 注意:第一次訪問項目來到這兒沒有session context = resolveSession(context); //校驗用戶登陸信息, 並放入context, 如果subject,session,和授權認證AuthenticationInfo中都沒有,將會從rememberMeManager(cookie)中獲取
//注意:第一次訪問項目來到這兒沒有這些信息 context = resolvePrincipals(context); //創建一個WebDelegatingSubject對象 Subject subject = doCreateSubject(context); //保存當前認證的用戶信息(一般是用戶名)在session里,並標記當前用戶已經被認證過 為了下次remember使用 save(subject); return subject; }
// 從context中獲取session protected SubjectContext resolveSession(SubjectContext context) { Session session = resolveContextSession(context); if (session != null) { context.setSession(session); } return context; } protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException { //調用的下面子類DefaultWebSecurityManager的方法 SessionKey key = getSessionKey(context); if (key != null) { //調用 SessionsSecurityManager#getSession return getSession(key); } return null; }
//登陸 public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info; try { info = authenticate(token); } catch (AuthenticationException ae) { onFailedLogin(token, ae, subject); } Subject loggedIn = createSubject(token, info, subject); //登陸成功后 根據配置的"記住我" 保存認證信息 onSuccessfulLogin(token, info, loggedIn); return loggedIn; } protected void onSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) { rememberMeSuccessfulLogin(token, info, subject); } protected void rememberMeSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) { //獲取 rememberMeManager管理器 RememberMeManager rmm = getRememberMeManager(); rmm.onSuccessfulLogin(subject, token, info); }
DefaultWebSecurityManager
//創建sessionKey
@Override protected SessionKey getSessionKey(SubjectContext context) { //從context中獲取sessonId和request,response if (WebUtils.isWeb(context)) { Serializable sessionId = context.getSessionId(); ServletRequest request = WebUtils.getRequest(context); ServletResponse response = WebUtils.getResponse(context); return new WebSessionKey(sessionId, request, response); } else { ... } } // 第一次調用時 getSession方法最后會調用到這兒來 返回null protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException { Serializable sessionId = getSessionId(sessionKey); if (sessionId == null) { return null; } Session s = retrieveSessionFromDataSource(sessionId); if (s == null) { //session ID was provided, meaning one is expected to be found, but we couldn't find one: String msg = "Could not find session with ID [" + sessionId + "]"; throw new UnknownSessionException(msg); } return s; }
現在開始執行請求路徑對應的過濾器
由於過濾鏈中的過濾器也是OncePerRequestFilte的子類,繼續走OncePerRequestFilte#doFilter方法 然后會調用第一步doFilterInternal方法
我們自定義的方法一般也是繼承了AdviceFilter過濾器
AdviceFilter
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { Exception exception = null; try {
//8、執行前置方法 boolean continueChain = preHandle(request, response); 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); } }
AccessControlFilter
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { //9、是登陸的rul或者已經認證過 否則重定向到登陸頁面 return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue); }
UserFilter
(這里以user過濾器為例,如果沒有認證過,直接重定向到登陸url)
該過濾器重寫了這兩個方法
//10、判斷是否認證過
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//判斷當前請求的路徑是否為當前過濾器配置的登陸路徑(登陸不需要任何權限,返回true) if (isLoginRequest(request, response)) { return true; } else {
//判斷是否已經認證過 Subject subject = getSubject(request, response); return subject.getPrincipal() != null; } } //11、返回false protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//重定向到登陸rul saveRequestAndRedirectToLogin(request, response); return false; }
//12、 protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException { //調用webUtils的方法,將當前請求失敗的的信息保存起來(便於下次認證成功后直接重定向到該路徑) saveRequest(request); //重定向的時候會生成session(shrio的) redirectToLogin(request, response); }
WebUtils
//保存
public static void saveRequest(ServletRequest request) { Subject subject = SecurityUtils.getSubject(); //13、這里會創建一個 StoppingAwareProxiedSession AbstractNativeSessionManager#start是創建simpleSession並調用session監聽器
//會將sessinID存在cookie中和sessionDao中(默認時ehcache緩存,可以自己實現redis等)(在DefaultSessionManager#create(Session session)方法中) Session session = subject.getSession(); HttpServletRequest httpRequest = toHttp(request); //將當前目標路徑的請求信息保存起來 SavedRequest savedRequest = new SavedRequest(httpRequest); //存到session中,跳到登陸頁面后登陸成功后會重定向到此次失敗的路徑 session.setAttribute(SAVED_REQUEST_KEY, savedRequest); } //重定向到savedRequet保存的路徑 如果是直接訪問的登陸url,則直接重定向到當前過濾器配置的登陸成功url //成功后的重定向可是不生成session的 public static void redirectToSavedRequest(ServletRequest request, ServletResponse response, String fallbackUrl) throws IOException { String successUrl = null; boolean contextRelative = true; //從session中獲取,並清空上一次失敗保存的信息 SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(request); //上一次請求失敗的保存的對象 而且是get請求(這里一般是直接瀏覽器輸入的url) 如果是post請求過來的(一般是表單),直接返回目標路徑 if (savedRequest != null && savedRequest.getMethod().equalsIgnoreCase(AccessControlFilter.GET_METHOD)) { successUrl = savedRequest.getRequestUrl(); contextRelative = false; }
//第一次請求時,successUrl為null, 登陸成功后,有值(上一次失敗的url) if (successUrl == null) { successUrl = fallbackUrl; }
//15、發出重定向 WebUtils.issueRedirect(request, response, successUrl, null, contextRelative); } public static void issueRedirect(ServletRequest request, ServletResponse response, String url, Map queryParams, boolean contextRelative) throws IOException { issueRedirect(request, response, url, queryParams, contextRelative, true); } // 會把sessionID寫在rul上, 下面的內容就不帶大家看了 public static void issueRedirect(ServletRequest request, ServletResponse response, String url, Map queryParams, boolean contextRelative, boolean http10Compatible) throws IOException { RedirectView view = new RedirectView(url, contextRelative, http10Compatible); view.renderMergedOutputModel(queryParams, toHttp(request), toHttp(response)); }
SavedRequest
// SavedRequest的構造方法 public SavedRequest(HttpServletRequest request) {
//當前請求的方式(get|post...) this.method = request.getMethod();
//當前請求的參數 this.queryString = request.getQueryString();
//當前請求失敗的路徑 this.requestURI = request.getRequestURI(); }
RedirectView (拼接重定向的參數請求頭、url加sessionID,url編碼等操作都由這兒進入)
public final void renderMergedOutputModel( Map model, HttpServletRequest request, HttpServletResponse response) throws IOException { // Prepare name URL. StringBuilder targetUrl = new StringBuilder(); if (this.contextRelative && getUrl().startsWith("/")) { targetUrl.append(request.getContextPath()); } targetUrl.append(getUrl());
//拼接請求參數 appendQueryProperties(targetUrl, model, this.encodingScheme); sendRedirect(request, response, targetUrl.toString(), this.http10Compatible); }
AbstractNativeSessionManager
//14、創建session時會調用
public Session start(SessionContext context) { //創建simpleSession Session session = createSession(context); //重置session時間 applyGlobalSessionTimeout(session);
//會將sessionID存到cookie onStart(session, context); //調用session的Listner notifyStart(session); //Don't expose the EIS-tier Session object to the client-tier: return createExposedSession(session, context); }
那么此時一個沒有授權的請求就執行完畢,現在就來到了我們的登陸界面
登陸使用authc過濾器
FormAuthenticationFilter
和上面的過程一樣,會判斷是否認證,如果沒有會執行onAccessDenied方法
// 這里需要該過濾器的 登陸url 和登陸所在的界面的url一樣 protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { //條件: 配置的該過濾器的登陸路徑和請求路徑相同 if (isLoginRequest(request, response)) { //1、HttpServletRequest 2、post請求 if (isLoginSubmission(request, response)) { return executeLogin(request, response); } else { //登陸頁面的url 請求方式為get return true; } } else { //如果一個請求路徑配置的authc過濾器,然后沒有登陸直接調用,會走到這里 //重定向到登陸頁面 會創建一個StoppingAwareProxiedSession類型的session 並把sessionId放在登陸頁面的url上 saveRequestAndRedirectToLogin(request, response); return false; } }
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { //調用 new UsernamePasswordToken(username, password, rememberMe, host); AuthenticationToken token = createToken(request, response); try { Subject subject = getSubject(request, response); subject.login(token); return onLoginSuccess(token, subject, request, response); } catch (AuthenticationException e) { return onLoginFailure(token, e, request, response); } } //登陸成功后 重定向到上一次重定向過來的路徑或者當前過濾器的登陸路徑 //可以重寫該方法登陸后直接跳到當前過濾器配置的url 而不是上一次失敗的url protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception { //調用父類AuthenticationFilter的issueSuccessRedirect方法 issueSuccessRedirect(request, response); //重定向后,阻止過濾連調用 return false; }
AuthenticationFilter
protected void issueSuccessRedirect(ServletRequest request, ServletResponse response) throws Exception { //當前過濾器配置的登陸url WebUtils.redirectToSavedRequest(request, response, getSuccessUrl()); }
DelegatingSubject
public void login(AuthenticationToken token) throws AuthenticationException { clearRunAsIdentitiesInternal(); //委托給securiManager登陸 Subject subject = securityManager.login(this, token); PrincipalCollection principals; String host = null; if (subject instanceof DelegatingSubject) { DelegatingSubject delegating = (DelegatingSubject) subject; //認證信息 principals = delegating.principals; host = delegating.host; } else { principals = subject.getPrincipals(); } this.principals = principals; //標記已經登陸過 this.authenticated = true; if (token instanceof HostAuthenticationToken) { host = ((HostAuthenticationToken) token).getHost(); } if (host != null) { this.host = host; } //獲取登陸時的session Session session = subject.getSession(false); if (session != null) { //執行new StoppingAwareProxiedSession(session, this); 登陸后的session封裝成StoppingAwareProxiedSession代理對象 this.session = decorate(session); } else { this.session = null; } }
AbstractRememberMeManager 處理 remeberme
public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) { //清空之前的認證信息 forgetIdentity(subject); //如果是rememberMe類型的token if (isRememberMe(token)) { //記錄 rememberIdentity(subject, token, info); } } public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) { //從認證后的信息中獲取 PrincipalCollection principals = getIdentityToRemember(subject, authcInfo); rememberIdentity(subject, principals); } // 加密處理 protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) { byte[] bytes = convertPrincipalsToBytes(accountPrincipals); rememberSerializedIdentity(subject, bytes); } //使用CipherService類進行處理 protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) { byte[] bytes = serialize(principals); if (getCipherService() != null) { bytes = encrypt(bytes); } return bytes; } // 返回加密后的認證信息 protected byte[] encrypt(byte[] serialized) { byte[] value = serialized; CipherService cipherService = getCipherService(); if (cipherService != null) { // getEncryptionCipherKey() 獲取的是rememberMe cookie加密和解密的密鑰 ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey()); value = byteSource.getBytes(); } return value; } //加密 protected byte[] encrypt(byte[] serialized) { byte[] value = serialized; CipherService cipherService = getCipherService(); if (cipherService != null) { ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey()); value = byteSource.getBytes(); } return value; }
CookieRememberMeManager
protected void rememberSerializedIdentity(Subject subject, byte[] serialized) { HttpServletRequest request = WebUtils.getHttpRequest(subject); HttpServletResponse response = WebUtils.getHttpResponse(subject); //base 64 encode it and store as a cookie: String base64 = Base64.encodeToString(serialized); //rememberMe的cookie模板 key為自定義的名字 我這兒是rememberMe Cookie template = getCookie(); Cookie cookie = new SimpleCookie(template); cookie.setValue(base64); cookie.saveTo(request, response); }
下面是remember的配置
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <constructor-arg value="rememberMe"/> <property name="httpOnly" value="true"/> <property name="maxAge" value="2592000"/><!-- 30天 --> </bean> <!-- rememberMe管理器 --> <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager"> <!-- rememberMe cookie加密和解密的密鑰 建議每個項目都不一樣 默認AES算法 密鑰長度(128 256 512 位)--> <property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}"/> <property name="cookie" ref="rememberMeCookie"/> </bean>
由於篇幅原因,本節詳細介紹的是登陸需要驗證的請求跳轉到登陸界面的源碼解析
小結:
1、當第一次請求失敗后,會重定向到當前過濾器的登陸界面,並創建一個session,將sessinID存在cookie,重定向的url,還會存放在sessionDao中(默認是ehcache, 可自定義)
2、當請求的路徑為不用認證(anon等自定義preHandle返回true的路徑),也會由servlet容器調用shiroRequest的getSession方法創建一個session,保存位置同1