1. 程式人生 > >Shiro原始碼分析----認證流程

Shiro原始碼分析----認證流程

    由於本文是基於原始碼分析Shiro認證流程,所以假設閱讀者對Shiro已經有一定的瞭解,如果對Shiro還不大瞭解的話,推薦一下博文:跟我學Shiro目錄貼


Apache Shiro作為一個優秀的許可權框架,其最重要的兩項工作:其一是認證,即解決登入的使用者的身份是否合法;其二是使用者登入後有什麼樣的許可權。本文將基於Shiro原始碼來剖析Shiro的認證流程,只有深層次的理解Shiro認證流程,認證過程中各個元件的作用,才能在實際應用中靈活使用。由於Shiro一般用於Web環境且會與Spring整合使用,所以此次認證流程的分析的前提也是Web環境且Shiro已與Spring整合。


特別說明:本文使用的Shiro版本:1.2.2。


Shiro與Spring整合時,需要在web.xml中配置Shiro入口過濾器:

<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()方法返回的:
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。


由於在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是如何創建出來的:
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了。


通過上述分析可以知道,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
    issueSuccessRedirect(request, response);
    // 由於返回false,所以過濾器鏈不再執行
    return false;
}

protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e,
                                 ServletRequest request, ServletResponse response) {
    // 設定錯誤資訊至request
    setFailureAttribute(request, e);
    // 由於返回true,所以過濾器鏈繼續執行,所以又返回了登入頁面
    return true;
}

至此,認證流程大致流程就是這樣了,限於篇幅,登入的流程具體,請期待下篇博文。