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精讲》。

相关文章
相关标签/搜索