因爲本文是基於源碼分析Shiro認證流程,因此假設閱讀者對Shiro已經有必定的瞭解。java
Apache Shiro做爲一個優秀的權限框架,其最重要的兩項工做:其一是認證,即解決登陸的用戶的身份是否合法;其二是用戶登陸後有什麼樣的權限。本文將基於Shiro源碼來剖析Shiro的認證流程,只有深層次的理解Shiro認證流程,認證過程當中各個組件的做用,才能在實際應用中靈活使用。因爲Shiro通常用於Web環境且會與Spring集成使用,因此這次認證流程的分析的前提也是Web環境且Shiro已與Spring集成。web
特別說明:本文使用的Shiro版本:1.2.2。spring
Shiro與Spring集成時,須要在web.xml中配置Shiro入口過濾器:apache
<filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <async-supported>true</async-supported> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
熟悉Spring的人應該都知道DelegatingFilterProxy
的做用,該Spring提供的過濾器只起委託做用,執行流程委託給Spring容器中名爲shiroFilter
的過濾器。因此還須要在Spring配置文件中配置shiroFilter
,以下:安全
<!-- Shiro的Web過濾器 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="filters"> <util:map> <entry key="authc" value-ref="formAuthenticationFilter"/> </util:map> </property> <property name="filterChainDefinitions"> <value> /index.jsp = anon /unauthorized.jsp = anon /login.jsp = authc /logout = logout /authenticated.jsp = authc /** = user </value> </property> </bean>
ShiroFilterFactoryBean
實現了org.springframework.beans.factory.FactoryBean
接口,因此shiroFilter
對象是由ShiroFilterFactoryBean
的getObject()
方法返回的:session
public Object getObject() throws Exception { if (instance == null) { instance = createInstance(); } return instance; } protected AbstractShiroFilter createInstance() throws Exception { log.debug("Creating Shiro Filter instance."); // 獲取配置文件中設置的安全管理器 SecurityManager securityManager = getSecurityManager(); if (securityManager == null) { String msg = "SecurityManager property must be set."; throw new BeanInitializationException(msg); } // 必須是Web環境的安全管理器 if (!(securityManager instanceof WebSecurityManager)) { String msg = "The security manager does not implement the WebSecurityManager interface."; throw new BeanInitializationException(msg); } // 建立過濾器鏈管理器 FilterChainManager manager = createFilterChainManager(); // 建立基於路徑匹配的過濾器鏈解析器 PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver(); chainResolver.setFilterChainManager(manager); // 返回SpringShiroFilter對象 return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver); }
從上述源碼中能夠看到,最終返回了一個SpringShiroFilter
對象,即Spring配置文件中的shiroFilter
對象,該過濾器擁有三個重要對象:SecurityManager
、PathMatchingFilterChainResolver
、FilterChainManager
。app
因爲在Spring配置中設置了filterChainDefinitions
屬性,因此會調用setFilterChainDefinitions
方法:框架
public void setFilterChainDefinitions(String definitions) { Ini ini = new Ini(); ini.load(definitions); //did they explicitly state a 'urls' section? Not necessary, but just in case: Ini.Section section = ini.getSection(IniFilterChainResolverFactory.URLS); if (CollectionUtils.isEmpty(section)) { //no urls section. Since this _is_ a urls chain definition property, just assume the //default section contains only the definitions: section = ini.getSection(Ini.DEFAULT_SECTION_NAME); } /** 獲取默認section,也就是加載 /index.jsp = anon /unauthorized.jsp = anon /login.jsp = authc /logout = logout /authenticated.jsp = authc /** = user 這段配置,從這段配置中能夠知道哪一種URL須要應用上哪些Filter,像anon、authc、logout就是Filter的名稱, Ini.Section實現了Map接口,其key爲URL匹配符,value爲Filter名稱 **/ // 設置filterChainDefinitionMap setFilterChainDefinitionMap(section); }
FilterChainManager
用於管理當前Shiro應用的全部Filter,有Shiro默認使用的Filter,也能夠是自定義的Filter。下面咱們看看FilterChainManager
是如何建立出來的:jsp
protected FilterChainManager createFilterChainManager() { // 建立DefaultFilterChainManager DefaultFilterChainManager manager = new DefaultFilterChainManager(); // 建立Shiro默認Filter,根據org.apache.shiro.web.filter.mgt.DefaultFilter建立 Map<String, Filter> defaultFilters = manager.getFilters(); //apply global settings if necessary: for (Filter filter : defaultFilters.values()) { // 設置相關Filter的loginUrl、successUrl、unauthorizedUrl屬性 applyGlobalPropertiesIfNecessary(filter); } // 獲取在Spring配置文件中配置的Filter Map<String, Filter> filters = getFilters(); if (!CollectionUtils.isEmpty(filters)) { for (Map.Entry<String, Filter> entry : filters.entrySet()) { String name = entry.getKey(); Filter filter = entry.getValue(); applyGlobalPropertiesIfNecessary(filter); if (filter instanceof Nameable) { ((Nameable) filter).setName(name); } // 將配置的Filter添加至鏈中,若是同名Filter已存在則覆蓋默認Filter manager.addFilter(name, filter, false); } } //build up the chains: Map<String, String> chains = getFilterChainDefinitionMap(); if (!CollectionUtils.isEmpty(chains)) { for (Map.Entry<String, String> entry : chains.entrySet()) { String url = entry.getKey(); String chainDefinition = entry.getValue(); // 爲配置的每個URL匹配建立FilterChain定義, // 這樣當訪問一個URL的時候,一旦該URL配置上則就知道該URL須要應用上哪些Filter // 因爲URL配置符會配置多個,因此以第一個匹配上的爲準,因此越具體的匹配符應該配置在前面,越寬泛的匹配符配置在後面 manager.createChain(url, chainDefinition); } } return manager; }
PathMatchingFilterChainResolver
對象職責很簡單,就是使用ant路徑匹配方法匹配訪問的URL,因爲pathMatchingFilterChainResolver
擁有FilterChainManager
對象,因此URL匹配上後能夠獲取該URL須要應用的FilterChain
了。async
經過上述分析能夠知道,Shiro就是經過一系列的URL匹配符配置URL應該應用上的Filter,而後在Filter中完成相應的任務,因此Shiro的全部功能都是經過Filter完成的。固然認證功能也不例外,在上述配置中認證功能是由org.apache.shiro.web.filter.authc.FormAuthenticationFilter
完成的。
下面咱們就看看入口過濾器SpringShiroFilter
的執行流程,是如何執行到FormAuthenticationFilter
的。既然是Filter,那麼最重要的就是doFilter
方法了,因爲SpringShiroFilter
繼承自OncePerRequestFilter
,doFilter
方法也是在OncePerRequestFilter
中定義的:
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 用於保證鏈中同一類型的Filter只會被執行一次 String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName(); if ( request.getAttribute(alreadyFilteredAttributeName) != null ) { log.trace("Filter '{}' already executed. Proceeding without invoking this filter.", getName()); filterChain.doFilter(request, response); } else //noinspection deprecation if (/* added in 1.2: */ !isEnabled(request, response) || /* retain backwards compatibility: */ shouldNotFilter(request) ) { log.debug("Filter '{}' is not enabled for the current request. Proceeding without invoking this filter.", getName()); filterChain.doFilter(request, response); } else { // Do invoke this filter... log.trace("Filter '{}' not yet executed. Executing now.", getName()); request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE); try { // 執行真正的功能代碼 doFilterInternal(request, response, filterChain); } finally { // Once the request has finished, we're done and we don't // need to mark as 'already filtered' any more. request.removeAttribute(alreadyFilteredAttributeName); } } }
doFilterInternal
方法定義AbstractShiroFilter
中:
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain) throws ServletException, IOException { Throwable t = null; try { final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain); final ServletResponse response = prepareServletResponse(request, servletResponse, chain); // 建立Subject對象,因而可知,每個請求到來,都會調用createSubject方法 final Subject subject = createSubject(request, response); // 經過Subject對象執行過濾器鏈, subject.execute(new Callable() { public Object call() throws Exception { // 更新會話最後訪問時間,用於計算會話超時 updateSessionLastAccessTime(request, response); // 執行過濾器鏈 executeChain(request, response, chain); return null; } }); } catch (ExecutionException ex) { t = ex.getCause(); } catch (Throwable throwable) { t = throwable; } // 省略一些代碼... }
先看一下,Subject
若是是如何建立的:
protected WebSubject createSubject(ServletRequest request, ServletResponse response) { return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject(); }
跟蹤代碼最終調用DefaultWebSubjectFactory.createSubject
方法:
public Subject createSubject(SubjectContext context) { if (!(context instanceof WebSubjectContext)) { return super.createSubject(context); } WebSubjectContext wsc = (WebSubjectContext) context; SecurityManager securityManager = wsc.resolveSecurityManager(); Session session = wsc.resolveSession(); boolean sessionEnabled = wsc.isSessionCreationEnabled(); PrincipalCollection principals = wsc.resolvePrincipals(); // 判斷是已經認證,若是是在沒有登陸以前,明顯返回是false boolean authenticated = wsc.resolveAuthenticated(); String host = wsc.resolveHost(); ServletRequest request = wsc.resolveServletRequest(); ServletResponse response = wsc.resolveServletResponse(); return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled, request, response, securityManager); }
接下來看看過濾器鏈是如何建立與執行的:
protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain) throws IOException, ServletException { // 獲取當前URL匹配的過濾器鏈 FilterChain chain = getExecutionChain(request, response, origChain); // 執行過濾器鏈中的過濾器 chain.doFilter(request, response); } protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) { FilterChain chain = origChain; // 獲取過濾器鏈解析器,即上面建立的PathMatchingFilterChainResolver對象 FilterChainResolver resolver = getFilterChainResolver(); if (resolver == null) { log.debug("No FilterChainResolver configured. Returning original FilterChain."); return origChain; } // 調用其getChain方法,根據URL匹配相應的過濾器鏈 FilterChain resolved = resolver.getChain(request, response, origChain); if (resolved != null) { log.trace("Resolved a configured FilterChain for the current request."); chain = resolved; } else { log.trace("No FilterChain configured for the current request. Using the default."); } return chain; }
根據上述Spring配置,假設如今第一次訪問URL: "/authenticated.jsp"
,則會應用上名爲authc
的Filter,即FormAuthenticationFilter
,根據FormAuthenticationFilter
的繼承體系,先執行dviceFilter.doFilterInternal
方法:
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { Exception exception = null; try { // 執行preHandle boolean continueChain = preHandle(request, response); if (log.isTraceEnabled()) { log.trace("Invoked preHandle method. Continuing chain?: [" + continueChain + "]"); } // 若是preHandle返回false則過濾器鏈再也不執行 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); } }
接下來執行:PathMatchingFilter.preHandle
方法:
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { if (this.appliedPaths == null || this.appliedPaths.isEmpty()) { if (log.isTraceEnabled()) { log.trace("appliedPaths property is null or empty. This Filter will passthrough immediately."); } return true; } for (String path : this.appliedPaths.keySet()) { // 根據配置,訪問URL:"/authenticated.jsp"時,會匹配上FormAuthenticationFilter, // 而FormAuthenticationFilter繼承自PathMatchingFilter,因此返回true if (pathsMatch(path, request)) { log.trace("Current requestURI matches pattern '{}'. Determining filter chain execution...", path); Object config = this.appliedPaths.get(path); // 執行isFilterChainContinued方法,該方法調用onPreHandle方法 return isFilterChainContinued(request, response, path, config); } } //no path matched, allow the request to go through: return true; }
接着執行AccessControlFilter.onPreHandle
方法:
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { // 若是isAccessAllowed方法返回false,則會執行onAccessDenied方法 return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue); }
接着執行AuthenticatingFilter.isAccessAllowed
方法:
@Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { return super.isAccessAllowed(request, response, mappedValue) || (!isLoginRequest(request, response) && isPermissive(mappedValue)); } super.isAccessAllowed方法,即AuthenticationFilter.isAccessAllowed方法: protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { Subject subject = getSubject(request, response); return subject.isAuthenticated(); }
由以上代碼可知,因爲是第一次訪問URL:"/authenticated.jsp"
,因此isAccessAllowed
方法返回false
,因此接着執行FormAuthenticationFilter.onAccessDenied
方法:
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { // 第一次訪問天然不是登陸請求 if (isLoginRequest(request, response)) { // 判斷是不是POST請求 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; } } protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException { // 將request對象保存在session中,以便登陸成功後從新轉至上次訪問的URL saveRequest(request); // 重定向至登陸頁面,即:"/login.jsp" redirectToLogin(request, response); }
根據配置,訪問URL:"/login.jsp"
時也會應用上FormAuthenticationFilter
,因爲是重定向因此發起的是GET
請求,因此isLoginSubmission()
返回false
,因此沒有執行executeLogin
方法,因此可以訪問/login.jsp
頁面。在登陸表單中應該設置action=""
,這樣登陸請求會提交至/login.jsp
,這時爲POST
請求,因此會執行executeLogin
方法:
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { // 根據表單填寫的用戶名密碼建立AuthenticationToken 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 subject = getSubject(request, response); // 執行Subject.login方法進行登陸 subject.login(token); // 若是登陸成功,重定向至上次訪問的URL return onLoginSuccess(token, subject, request, response); } catch (AuthenticationException e) { // 若是登陸失敗,則設置錯誤信息至request,並從新返回登陸頁面 return onLoginFailure(token, e, request, response); } } protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception { // 重定向至上次訪問的URL issueSucce***edirect(request, response); // 因爲返回false,因此過濾器鏈再也不執行 return false; } protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { // 設置錯誤信息至request setFailureAttribute(request, e); // 因爲返回true,因此過濾器鏈繼續執行,因此又返回了登陸頁面 return true; }
至此,認證流程大體流程就是這樣了,限於篇幅,登陸的流程具體,請期待下篇博文。
-------------------------------- END -------------------------------
及時獲取更多精彩文章,請關注公衆號《Java精講》。