由于本文是基于源码分析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对象是由ShiroFilterFactoryBeangetObject()方法返回的:

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对象,该过滤器拥有三个重要对象:SecurityManagerPathMatchingFilterChainResolverFilterChainManager

由于在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继承自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方法:

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

©著作权归作者所有:来自51CTO博客作者Java精讲的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. IP地址分类及网络配置方法和多网卡绑定技术应用
  2. 大数据成神之路-Java高级特性增强(多线程)
  3. TP6 linux安装方法
  4. 硬盘显示没有初始化恢复资料方法
  5. 中了exe病毒文件夹变exe应用程序解决方法
  6. 磁盘出现“文件系统变RAW”的解决方法
  7. 存储卡出现“无法访问此卷不包含可识别的文件系统”的解决方法
  8. 【面试】PHP 字符串翻(反)转的几种方法
  9. 【面试】两个变量进行交替的N种方法

随机推荐

  1. sqoop简单操作-从mysql导入导出数据
  2. vs2012利用MFC开发基于对话框的小软件指
  3. 访问SqlServer时需要先登录服务器windows
  4. SQL问题 ,要连接两个无直接关联表
  5. 1)如何用语句来查看一个表内是否建了索引
  6. Scripts:创建手工的SQL PROFILE的脚本,老
  7. 求一SQL语句(如何按某列的值分组且取出每
  8. 高手是怎样炼成的:精妙SQL语句介绍
  9. 手把手教你mysql(十五)游标变量流程控制
  10. CentOS 7 安装MyCli MySQL 客户端