Shiro 自定义 filter 匹配异常(好文)

[转](http://www.hillfly.com/2017/179.html

最近忙着研究在 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 是从数据库读取的,如下:


image.png

我们想要达到的效果是,除了登陆和访问 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 加载的顺序是否异常。

image.png

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

然而奇怪的是,从调试结果来看,一切加载的 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.png

MMP 的,完全没有不是按我们预想的那样进行调用。这 TM 居然是作为 Spring 的全局 Filter 被调用了。Shiro 的 Filter 优先级居然失效了?我们都知道之前在 SpringMVC+Shiro 时,都会把 Shiro 的 Filter 配置顺序尽量放前,以达到优先加载的目的。难道这里没有走 Shiro 的匹配是因为这个吗??难道是因为 Springboot 先加载了我们自定义的 Filter,然后再加载了 ShiroFilter 吗,然后这个 Filter 优先顺序就出问题了?

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


image.png

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

@Bean
public ShiroFilterFactoryBean shiroFilter(){}

@Bean
public AccessTokenFilter accessTokenFilter(){}

image.png

果然顺序变了,那么问题解决了吗?
——没有,问题依旧,咱们的 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());
    //省略
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容