Shiro源碼分析----認證流程

因爲本文是基於源碼分析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對象是由ShiroFilterFactoryBeangetObject()方法返回的: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對象,該過濾器擁有三個重要對象:SecurityManagerPathMatchingFilterChainResolverFilterChainManagerapp

因爲在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繼承自OncePerRequestFilterdoFilter方法也是在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精講》。

相關文章
相關標籤/搜索