springboot整合shiro的文章到处都是。包括springboot的官网都有相应的例子。但是这块有个注意点,需要那些从springmvc迁到springboot的朋友注意下。这个问题困扰我了两三天,今天分享出来让后来人少踩坑。

spring整合shiro的时候我们会配置一个shiro.xml文件,将shiro的配置信息全部配置进去然后在web.xml里面配置一个过滤器代理就足够了。

<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>

但是到了springboot情况有所不同。之前我按照之前的方法原原本本的将shiro配置都写到了java文件中,并没有发现什么问题。可是真的使用的时候我发现了一个问题--过滤器链在无限次的自循环执行!

过滤器链后面连着servlet是一个完整的uri请求执行的路径。可是过滤器链自己无限次的循环执行,servlet根本执行不找,那么view也就不可能返回给前台页面,导致页面总是报连接异常。

找这个问题我也是走了很多的弯路。首先查找配置文件是不是配置错误了,找了几遍和网上写的都是一致的。然后一直在跟jetty的ServletHandler这个类。

@Override
        public void doFilter(ServletRequest request, ServletResponse response)
            throws IOException, ServletException
        {
            final Request baseRequest=Request.getBaseRequest(request);

            // pass to next filter
            if (_filterHolder!=null)
            {
                if (LOG.isDebugEnabled())
                    LOG.debug("call filter {}", _filterHolder);
                Filter filter= _filterHolder.getFilter();
                
                //if the request already does not support async, then the setting for the filter
                //is irrelevant. However if the request supports async but this filter does not
                //temporarily turn it off for the execution of the filter
                if (baseRequest.isAsyncSupported() && !_filterHolder.isAsyncSupported())
                { 
                    try
                    {
                        baseRequest.setAsyncSupported(false,_filterHolder.toString());
                        filter.doFilter(request, response, _next);
                    }
                    finally
                    {
                        baseRequest.setAsyncSupported(true,null);
                    }
                }
                else
                    filter.doFilter(request, response, _next);

                return;
            }

            // Call servlet
            HttpServletRequest srequest = (HttpServletRequest)request;
            if (_servletHolder == null)
                notFound(baseRequest, srequest, (HttpServletResponse)response);
            else
            {
                if (LOG.isDebugEnabled())
                    LOG.debug("call servlet " + _servletHolder);
                _servletHolder.handle(baseRequest,request, response);
            }
        }

我很疑惑为什么这里面的_servletHolder为空!!

后来我想过滤器链无限循环就关注过滤器链。现在我配置了a,b,c,d四个过滤器,这四个过滤器应该被包装在spring的DelegatingFilterProxy过滤器代理e中,而DelegatingFilterProxy过滤器代理会被包装成FilterRegistrationBean类型的f。也就是说过滤器链应该是形如这样的

e(a,b,c,d包含其中)->f-Dispacher

而我发现项目中的过滤器是酱紫的:a->b->c->d->e->f->Dispacher

其中c过滤器(LogoutFilter)过滤了所有请求,使用了shiro的页面跳转规则

WebUtils类
 public static void issueRedirect(ServletRequest request, ServletResponse response, String url, Map queryParams, boolean contextRelative, boolean http10Compatible) throws IOException {
        RedirectView view = new RedirectView(url, contextRelative, http10Compatible);
        view.renderMergedOutputModel(queryParams, toHttp(request), toHttp(response));
    }

我们配置的spring跳转规则自然会无法正常跳转。这是无语啊~~

后来想想,spring大费周章的将这些过滤器代理了,到了springboot又来一个FilterRegistrationBean包装spring的代理过滤器。而现在这些过滤器裸露在外,肯定是配置的问题。

于是按照以下配置修改了xml的配置

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="${shiro.login.url}"/>
        <property name="unauthorizedUrl" value="${shiro.unauthorizedUrl}"/>
        <property name="filters">
            <util:map>
                <entry key="authc">
                    <bean id="formAuthenticationFilter"
                          class="*.*.a">
                      
                    </bean>
                </entry>
                <entry key="logout">
                    <bean id="logoutFilter" class="*.*.b">
                </entry>
                <entry key="sysUser">
                    <bean id="sysUserFilter" class="*.*.c">
                       
                    </bean>
                </entry>
                <entry key="onlineSession">
                    <bean id="onlineSessionFilter"
                          class="*.*.d">
                        
                    </bean>
                </entry>
                <entry key="syncOnlineSession">
                    <bean id="syncOnlineSessionFilter"
                          class="*.*.e">
                        <property name="onlineSessionDAO" />
                    </bean>
                </entry>
                <entry key="myfilter">
                    <bean id="myFilter" class="*.*.f"/>
                </entry>
            </util:map>
        </property>
        <property name="filterChainDefinitions">
            <value>
                /static/** = anon
                /js/** =anon
                /css/** =anon
                /favicon.ico =anon
                /images/** = anon
                /logout = logout
                /user/login=authc
                /** =sysUser,onlineSession,syncOnlineSession,perms,roles
            </value>
        </property>
    </bean>

这样的配置后,过滤器链恢复了正常,这些自定义的过滤器包装在了代理过滤器中。

这个问题之所以是个问题,还在于对以下几个问题没搞清楚:

1、FilterRegistrationBean类的实现机制

2、shiro过滤器链的实现机制

下面我就从这两个方面说说我最近看到的。

#FilterRegistrationBean类的实现机制

说到这个类我们先来说说在没有整合springboot的时候我们在web.xml里面配置的类DelegatingFilterProxy。

在web.xml里面我们只要将真正的shiroFilter的id和这个类的过滤器名称配置一致就能够实现代理。

这个原理很简单,DelegatingFilterProxy这个类会根据这个id名称在spring容器中找到这个类。

protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
		Filter delegate = wac.getBean(getTargetBeanName(), Filter.class);
		if (isTargetFilterLifecycle()) {
			delegate.init(getFilterConfig());
		}
		return delegate;
	}

然后调用shiroFilter的init方法,将我们配置的一堆过滤器和一堆映射转换成相应的字段信息

public void init() throws Exception {
        WebEnvironment env = WebUtils.getRequiredWebEnvironment(getServletContext());

        setSecurityManager(env.getWebSecurityManager());

        FilterChainResolver resolver = env.getFilterChainResolver();
        if (resolver != null) {
            setFilterChainResolver(resolver);
        }
    }
}

到了springboot里面又有加了这个类FilterRegistrationBean,将代理过滤器又包装了起来。主要是因为springboot在启动spring容器的时候会调用以下方法将过滤器加载到servlet上下文。

FilterRegistration.Dynamic added = servletContext.addFilter(name, filter);

也就是说之前我们的delegate配置到了web.xml直接能够放到servlet上下文。到了springboot它不默认去读web.xml。那么到springboot中delegateFilter是通过这个FilterRegistrationBean类放置到servlet上下文的。

下次再加载什么类,是不是可以想想这个类。

2、shiro过滤器链的实现机制

shiro的过滤器链算是比较重要的部分。今天有必要大体了解下。

shiro中包括自定义过滤器和shiro自己的过滤器又很多很多。他们组织在一起的机制是什么呢?

shiro的filter是一个工厂类ShiroFilterFactoryBean生产出来的,生产出来的过滤器类型是SpringShiroFilter。

这个类型里面包含了两个参数:一个是安全管理器,一个是过滤器链解析器

PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
        chainResolver.setFilterChainManager(manager);

        //Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built
        //FilterChainResolver.  It doesn't matter that the instance is an anonymous inner class
        //here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts
        //injection of the SecurityManager and FilterChainResolver:
        return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);

spring过滤器设置请求头 spring过滤器配置_java

从图中可以看出SpringShiroFilter实现了servlet3.0的Filter接口,并且继承了OncePerRequestFilter类。所以它既是一个普通的类,有是包含了shiro特性的代理类。 无论是shiroFilter还是代理过滤器SpringShiroFilter都是继承自AbstractShiroFilter,过滤器执行方法如下:

protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain)
            throws IOException, ServletException {
        FilterChain chain = getExecutionChain(request, response, origChain);
        chain.doFilter(request, response);
    }

chain的类型是代理过滤器链ProxiedFilterChain,这样shiro的内部过滤器链执行过程和其他的过滤器链执行过程很类似。

public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
        if (this.filters == null || this.filters.size() == this.index) {
            //we've reached the end of the wrapped chain, so invoke the original one:
            if (log.isTraceEnabled()) {
                log.trace("Invoking original filter chain.");
            }
            this.orig.doFilter(request, response);
        } else {
            if (log.isTraceEnabled()) {
                log.trace("Invoking wrapped filter at index [" + this.index + "]");
            }
            this.filters.get(this.index++).doFilter(request, response, this);
        }
    }

有了代理类SpringShiroFilter,它代理的那些过滤器都有一个特点:继承自AdviceFilter 该类有以下部分组成: ##过滤器链继续执行前

protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        return true;
    }

继承类可以覆盖该方法在过滤器链执行前,做一些自己想要的操作。 ##过滤链执行

rotected void executeChain(ServletRequest request, ServletResponse response, FilterChain chain) throws Exception {
        chain.doFilter(request, response);
    }

这个是过滤器链执行过程。 ##过滤器链执行完以后(正常)

protected void postHandle(ServletRequest request, ServletResponse response) throws Exception {
    }

过滤器链执行之后执行该方法,继承类可以覆盖该方法在过滤器链执行后,做一些想要的操作。 ##过滤链执行完成

public void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception {
    }

过滤器链在执行的过程中可能抛出异常,这个时候过滤器之后的操作都将无法执行。但是afterCompletion方法无论过滤器链在正常完成或是异常完成的情况下都会被执行。该方法特别适合完成一些后期的清除工作。(类似finnally原理)。

有了执行功能的过滤器,有了这些过滤器的代理过滤器。那么还有一个问题就清楚过滤器了:过滤器执行。

#过滤器执行 代理过滤器实现了接口AbstractShiroFilter。它内部的过滤器是通过方法doFilterInternal实现的。

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);

            final Subject subject = createSubject(request, response);

            //noinspection unchecked
            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;
        }

1、包装shiro的request类。

2、包装shiro的response类。

3、创建subject实例

4、调用subject实例的execute方法执行callable类的call()方法。

5、call()方法做了两件事情:更新session最新访问时间,执行shiro内部能够处理本次request请求的过滤器链(通过过滤器链解析器过滤出来的),并执行。

执行过程很巧妙,将任务包装成callable并将其放入subject的execute方法中。

public <V> V execute(Callable<V> callable) throws ExecutionException {
        Callable<V> associated = associateWith(callable);
        try {
            return associated.call();
        } catch (Throwable t) {
            throw new ExecutionException(t);
        }
    }

该方法将callable继续包装成SubjectCallable实例。

public <V> Callable<V> associateWith(Callable<V> callable) {
        return new SubjectCallable<V>(this, callable);
    }

最后调用的是SubjectCallable的call方法。

public V call() throws Exception {
        try {
            threadState.bind();
            return doCall(this.callable);
        } finally {
            threadState.restore();
        }
    }

call方法做了两件事情:

1、获取该线程的subject信息,并且将securityManager绑定到该线程中。

2、调用doCall方法执行过滤器链。

过滤器链是shiro的核心,但是不恰当的配置会导致过滤器链死循环执行。理解shiro的过滤器链实现原理,能够在我们排解相关错误做一个基础。