Shiro 自定义 filter 匹配异常

最近忙着研究在 Springboot 上使用 Shiro 的问题。刚好就遇到个诡异事,百度 Google 也没找到啥有价值的信息,几番周折自己解决了,这里稍微记录下。

自定义 Filter

Shiro 支持自定义 Filter 大家都知道,也经常用,这里我也用到了一个自定义 Filter,主要用于验证接口调用的 AccessToken 是否有效。

// AccessTokenFilter.java

public class AccessTokenFilter extends AccessControlFilter {
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest,
                                      ServletResponse servletResponse,
                                      Object o) {
        if (isValidAccessToken(request)) {
            return true;
        }
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, 
                                    ServletResponse servletResponse) throws Exception {
        throw new UnAuthorizedException("操作授权失败!" + SysConstant.ACCESSTOKEN + "失效!");
    }
}
// ShiroConfiguration.java

@Bean
public AccessTokenFilter accessTokenFilter(){
    return new AccessTokenFilter();
}

 @Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager,
                                          IUrlFilterService urlFilterService) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    shiroFilterFactoryBean.setSecurityManager(securityManager);

    // 自定义过滤器
    Map<String, Filter> filterMap = shiroFilterFactoryBean.getFilters();
    filterMap.put("hasToken", accessTokenFilter());
    shiroFilterFactoryBean.setFilters(filterMap);

    // URL过滤
    Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
    List<UrlFilter> urlFilterList = urlFilterService.selectAll();
    for (UrlFilter filter : urlFilterList) {
        filterChainDefinitionMap.put(filter.getFilterUrl(),
                filter.getFilterList());
    }

    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    return shiroFilterFactoryBean;
}

ShiroFilter 中的 FilterChain 是从数据库读取的,如下:

id url filter sort
1 /druid/* anon 1
2 /api/login anon 2
3 /** hasToken,authc 3

我们想要达到的效果是,除了登陆和访问 Druid 监控页面外,访问其它地址一律要先验证 Token,即走我们的自定义过滤器。
修改完毕后启动无异常,我们访问地址验证下。

# POST /api/login
{
  "hasError": true,
  "errors": {
    "httpStatus": 401,
    "errorCode": "4001",
    "errorMsg": "授权异常:操作授权失败!AccessToken失效!",
    "timestamp": "2017-06-10 11:08:03"
  }
}

funny,结果出乎意料,居然登陆接口走了咱们的那个自定义 Filter??黑人问号脸。。。

问题排查

FilterChain

首先检查 Shiro FilterChain 加载的顺序是否异常。

  1. 集合容器使用 LinkedHashMap,保证的 FilterChain 的顺序。
  2. 从数据库读取 Filter 时也是按 sort 排序的。
    从调试结果来看,加载顺序和数据并没有任何问题,都是正确的。

排除了自身的数据问题,那就要往深处挖掘原因了,有了之前解决 Quartz 问题的经历,这次毫不犹豫就决定跟源码跟踪 Filter 注册到匹配的过程。

Filter 注册

要查明白为何匹配异常,就要先弄清楚咱们的自定义 Filter 是如何注册到 Shiro 的,显然,问题的关键在于 ShiroFilter 返回的 ShiroFilterFactoryBean 这个类中,我们打开看看。很快,我们就锁定了关键 method:

//ShiroFilterFactoryBean.java

protected AbstractShiroFilter createInstance() throws Exception {
    log.debug("Creating Shiro Filter instance.");
    SecurityManager securityManager = this.getSecurityManager();
    String msg;
    if(securityManager == null) {
        msg = "SecurityManager property must be set.";
        throw new BeanInitializationException(msg);
    } else if(!(securityManager instanceof WebSecurityManager)) {
        msg = "The security manager does not implement the WebSecurityManager interface.";
        throw new BeanInitializationException(msg);
    } else {
        FilterChainManager manager = this.createFilterChainManager();
        PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
        chainResolver.setFilterChainManager(manager);
        return new ShiroFilterFactoryBean.SpringShiroFilter((WebSecurityManager)securityManager, chainResolver);
    }
}

protected FilterChainManager createFilterChainManager() {
    DefaultFilterChainManager manager = new DefaultFilterChainManager();
    Map<String, Filter> defaultFilters = manager.getFilters();
    Iterator var3 = defaultFilters.values().iterator();

    while(var3.hasNext()) {
        Filter filter = (Filter)var3.next();
        this.applyGlobalPropertiesIfNecessary(filter);
    }

    Map<String, Filter> filters = this.getFilters();
    String name;
    Filter filter;
    if(!CollectionUtils.isEmpty(filters)) {
        for(Iterator var10 = filters.entrySet().iterator(); var10.hasNext(); manager.addFilter(name, filter, false)) {
            Entry<String, Filter> entry = (Entry)var10.next();
            name = (String)entry.getKey();
            filter = (Filter)entry.getValue();
            this.applyGlobalPropertiesIfNecessary(filter);
            if(filter instanceof Nameable) {
                ((Nameable)filter).setName(name);
            }
        }
    }

    Map<String, String> chains = this.getFilterChainDefinitionMap();
    if(!CollectionUtils.isEmpty(chains)) {
        Iterator var12 = chains.entrySet().iterator();

        while(var12.hasNext()) {
            Entry<String, String> entry = (Entry)var12.next();
            String url = (String)entry.getKey();
            String chainDefinition = (String)entry.getValue();
            manager.createChain(url, chainDefinition);
        }
    }

    return manager;
}
//DefaultFilterChainManager.java
public DefaultFilterChainManager() {
    this.addDefaultFilters(false);
}

//DefaultFilter.java
public enum DefaultFilter {
    anon(AnonymousFilter.class),
    authc(FormAuthenticationFilter.class),
    authcBasic(BasicHttpAuthenticationFilter.class),
    logout(LogoutFilter.class),
    noSessionCreation(NoSessionCreationFilter.class),
    perms(PermissionsAuthorizationFilter.class),
    port(PortFilter.class),
    rest(HttpMethodPermissionFilter.class),
    roles(RolesAuthorizationFilter.class),
    ssl(SslFilter.class),
    user(UserFilter.class);
}

看到这总算弄清楚 Shiro 加载 Filter 的顺序:

  1. 加载 DefaultFilter 中的默认 Filter;
  2. 加载自定义 Filter;
  3. 加载 FFilterChainDefinitionMap;

弄清楚了这 Filter 的加载与注册,那这与我们要解决的问题有何关系呢?首先我们怀疑这里获取的 Filter 是异常的,调试打个断点看看。
image
然而奇怪的是,从调试结果来看,一切加载的 Filter 都如我们预想的那样,并无异常。

Filter Match

既然基本排除了 Filter 加载上出现问题的可能,那么就要来排查 Filter 匹配的问题了。
重点在于 AbstractShiroFilter 的 doFilterInternal (),这里是匹配的起点。

protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain) throws ServletException, IOException {
    Throwable t = null;
    try {
        final ServletRequest request = this.prepareServletRequest(servletRequest, servletResponse, chain);
        final ServletResponse response = this.prepareServletResponse(request, servletResponse, chain);
        Subject subject = this.createSubject(request, response);
        subject.execute(new Callable() {
            public Object call() throws Exception {
                AbstractShiroFilter.this.updateSessionLastAccessTime(request, response);
                AbstractShiroFilter.this.executeChain(request, response, chain);
                return null;
            }
        });
    } catch (ExecutionException var8) {
        t = var8.getCause();
    } catch (Throwable var9) {
        t = var9;
    }

    if(t != null) {
        if(t instanceof ServletException) {
            throw (ServletException)t;
        } else if(t instanceof IOException) {
            throw (IOException)t;
        } else {
            String msg = "Filtered request failed.";
            throw new ServletException(msg, t);
        }
    }
}

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

跟踪到最后,会进入到一个关键方法:

//PathMatchingFilterChainResolver.java

public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
    FilterChainManager filterChainManager = this.getFilterChainManager();
    if(!filterChainManager.hasChains()) {
        return null;
    } else {
        String requestURI = this.getPathWithinApplication(request);
        Iterator var6 = filterChainManager.getChainNames().iterator();
        String pathPattern;
        do {
            if(!var6.hasNext()) {
                return null;
            }
            pathPattern = (String)var6.next();
        } while(!this.pathMatches(pathPattern, requestURI));

        if(log.isTraceEnabled()) {
            log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "].  Utilizing corresponding filter chain...");
        }
        return filterChainManager.proxy(originalChain, pathPattern);
    }
}

显然,这里就是进行 URL 匹配的地方。难道是这里匹配出异常了?我们打个断点在这里再访问一下。然而怪异出现了,没有进断点,直接返回了异常信息,根本没有进行匹配!!我们再对自定义 filter 断点调试后发现了 Filter 调用链如下:
image
MMP 的,完全没有不是按我们预想的那样进行调用。这 TM 居然是作为 Spring 的全局 Filter 被调用了。Shiro 的 Filter 优先级居然失效了?我们都知道之前在 SpringMVC+Shiro 时,都会把 Shiro 的 Filter 配置顺序尽量放前,以达到优先加载的目的。难道这里没有走 Shiro 的匹配是因为这个吗??难道是因为 Springboot 先加载了我们自定义的 Filter,然后再加载了 ShiroFilter 吗,然后这个 Filter 优先顺序就出问题了?

我们将断点打到 ApplicationFilterChain.java 的 internalDoFilter () 中进行验证下:


!!果然啊!咱们的自定义 Filter 居然还在 ShiroFilter 之前,这就导致请求被我们自定义 Filter 先消费掉了。。ShiroFilter 成了摆设。
那么把咱们的 Bean 放到 ShiroFilter 后面会如何呢?

@Bean
public ShiroFilterFactoryBean shiroFilter(){}

@Bean
public AccessTokenFilter accessTokenFilter(){}


果然顺序变了,那么问题解决了吗?
—— 没有,问题依旧,咱们的 Filter 还是跑了,返回了异常。

看来应该不是这里的顺序问题,我们回过头来继续看 ApplicationFilterChain.java 的 internalDoFilter (),系统会将注册的 filters 逐一调用,也就是说无论我们的顺序如何,Filter 最终都是会被调用的。

问题解决

眼下我暂时有两种办法去解决这个问题:

  1. 修改 AccessTokenFilter,在 Filter 内部加入 path match 方法对需要验证 token 的路径进行过滤。
  2. 将咱们的自定义 Filter 注册到 Shiro,不注册到 ApplicationFilterChain。

显然方案一是不可取的,这样修改范围过大,得不偿失了。那我们怎么去实现第二个方法呢?SpringBoot 提供了 FilterRegistrationBean 方便我们对 Filter 进行管理。

@Bean
public FilterRegistrationBean registration(AccessTokenFilter filter) {
    FilterRegistrationBean registration = new FilterRegistrationBean(filter);
    registration.setEnabled(false);
    return registration;
}

将不需要注册的 Filter 注入方法即可。这时候再启动项目进行测试,就可以发现 filters 已经不存在咱们的自定义 Filter 了。

还有个办法不需要使用到 FilterRegistrationBean,因为我们将 AccessTokenFilter 注册为了 Bean 交给 Spring 托管了,所以它会被自动注册到 FilterChain 中,那我们如果不把它注册为 Bean 就可以避免这个问题了。

/**
 * 不需要显示注册Bean了
@Bean
public AccessTokenFilter accessTokenFilter(){}
**/

@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager,
                                          IUrlFilterService urlFilterService) {
    //省略
    filterMap.put("hasToken", new AccessTokenFilter());
    //省略
}

原文:http://www.hillfly.com/2017/179.html