zoukankan      html  css  js  c++  java
  • Shiro权限管理框架(五):自定义Filter实现及其问题排查记录

    明确需求

    在使用Shiro的时候,鉴权失败一般都是返回一个错误页或者登录页给前端,特别是后台系统,这种模式用的特别多。但是现在的项目越来越多的趋向于使用前后端分离的方式开发,这时候就需要响应Json数据给前端了,前端再根据状态码做相应的操作。那么Shiro框架能不能在鉴权失败的时候直接返回Json数据呢?答案当然是可以。

    其实Shiro的自定义过滤器功能特别强大,可以实现很多实用的功能,向前端返回Json数据自然不在话下。通常我们没有去关注它是因为Shiro内置的一下过滤器功能已经比较全了,后台系统的权限控制基本上只需要使用Shiro内置的一些过滤器就能实现了,此处再次贴上这个图。

    这已经是我第三次贴这张图了

    相关文档地址:http://shiro.apache.org/web.html#default-filters

    我最近的一个项目是需要为手机APP提供功能接口,需要做用户登录,Session持久化以及Session共享,但不需要细粒度的权限控制。面对这个需求我第一个想到的就是集成Shiro了,Session的持久化及共享在Shiro系列第二篇已经讲过了,那么这篇顺便用一下Shiro中的自定义过滤器。因为不需要提供细粒度权限控制,只需要做登录鉴权,而且鉴权失败后需要向前端响应Json数据,那么使用自定义Filter再好不过了。

    自定义Filter

    还是以第一篇的Demo为例,项目地址在文章尾部有放上,本篇在之前的代码上继续添加功能。

    首发地址:https://www.guitu18.com/post/2020/01/06/64.html

    在实现自定义Filter之前,我们先看看这个类:org.apache.shiro.web.filter.AccessControlFilter,点开它的子类,发现子类全部都是org.apache.shiro.web.filter.authcorg.apache.shiro.web.filter.authz这两个包下的,大多都继承了AccessControlFilter这个类。这些子类的类名是不是很眼熟,看上面那张我贴了三遍的图,大部分都在这里面呢。

    看来AccessControlFilter这个类是跟Shiro权限过滤密切相关的,那么先看看它的体系结构:

    它的顶级父类是javax.servlet.Filter,前面我们也说过,Shiro中所有的权限过滤都是基于Filter来实现的。自定义Filter同样需要实现AccessControlFilter,这里我们添加一个登录验证过滤器,代码如下:

    public class AuthLoginFilter extends AccessControlFilter {
        // 未登录登陆返状态回码
        private int code;
        // 未登录登陆返提示信息
        private String message;
        public AuthLoginFilter(int code, String message) {
            this.code = code;
            this.message = message;
        }
        @Override
        protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse,
                                          Object mappedValue) throws Exception {
            Subject subject = SecurityUtils.getSubject();
            // 这里配合APP需求我只需要做登录检测即可
            if (subject != null && subject.isAuthenticated()) {
                // TODO 登录检测通过,这里可以添加一些自定义操作
                return Boolean.TRUE;
            }
            // 登录检测失败返货False后会进入下面的onAccessDenied()方法
            return Boolean.FALSE;
        }
        @Override
        protected boolean onAccessDenied(ServletRequest servletRequest, 
                                         ServletResponse servletResponse) throws Exception {
            PrintWriter out = null;
            try {
                // 这里就很简单了,向Response中写入Json响应数据,需要声明ContentType及编码格式
                servletResponse.setCharacterEncoding("UTF-8");
                servletResponse.setContentType("application/json; charset=utf-8");
                out = servletResponse.getWriter();
                out.write(JSONObject.toJSONString(R.error(code, message)));
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (out != null) {
                    out.close();
                }
            }
            return Boolean.FALSE;
        }
    }
    

    自定义过滤器写好了,现在需要把它交给Shiro管理:

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 添加登录过滤器
        Map<String, Filter> filters = new LinkedHashMap<>();
        // 这里注释的一行是我这次踩的一个小坑,我一开始按下面这么配置产生一个我意料之外的问题
        // filters.put("authLogin", authLoginFilter());
        // 正确的配置是需要我们自己new出来,不能将这个Filter交给Spring管理,后面会说明
        filters.put("authLogin", new AuthLoginFilter(500, "未登录或登录超时"));
        shiroFilterFactoryBean.setFilters(filters);
        // 设置过滤规则
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/api/login", "anon");
        filterMap.put("/api/**", "authLogin");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
        return shiroFilterFactoryBean;
    }
    

    如此Shiro添加自定义过滤器就完成了。自定义的Filter可以添加多个以实现不同的需求,你仅仅需要在filters中将过滤器起好名字put进去,并在filterChainMap中添加过滤器别名和路径的映射就可以使用这个过滤器了。需要注意的一点就是过滤器是从前往后顺序匹配的,所以要把范围大的路径放在后面put进去。

    到这里自定义Filter功能已经实现了,后面是采坑排查记录,不感兴趣可以跳过。

    问题排查

    前半段介绍了如何使用Shiro的自定义Filter功能实现过滤,在Shiro配置代码中我提了一句这次配置踩的一个小坑,如果我们将自定义的Filter交给Spring管理,会产生一些意料之外的问题。确实,通常在Spring项目中做配置时,我们都默认将Bean交由Spring管理,一般不会有什么问题,但是这次不一样,先看代码如下:

    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ...
        filters.put("authLogin", authLoginFilter());
        ...
        filterMap.put("/api/login", "anon");
        filterMap.put("/api/**", "authLogin");
        ...
    }
    @Bean
    public AuthLoginFilter authLoginFilter() {
        return new AuthLoginFilter(500, "未登录或登录超时");
    }
    

    这样配置后造成的现象是:无论前面的过滤器是否放行,最终都会走到自定义的AuthLoginFilter过滤器

    比如上面的配置,我们访问/api/login正常来讲会被anon匹配到AnonymousFilter中,这里是什么都没做直接放行的,但是放行后还会继续走到AuthLoginFilter中,怎么会这样,说好的按顺序匹配呢,怎么不按套路出牌。

    打断点一路往上追溯,我们找到了ApplicationFilterChain这里,它是Tomcat所实现的一个Java Servlet API的规范。所有的请求都必须通过filters里的过滤器层层过滤后才会调用Servlet中的方法service()方法。这里包括Spring中的各种过滤器,全部都是注册到这里来的。

    前面的四个Filter都是Spring的,第五个是ShiroShiroFilterFactoryBean,它的内部也维护了一个filters,用来保存Shiro内置的一些过滤器和我们自定义的过滤器,Tomcat所维护的filtersShiro维护的filters是一个父子层级的关系Shiro中的ShiroFilterFactoryBean仅仅只是Tomcatfilters中的一员。点开看ShiroFilterFactoryBean查看,果然Shiro内置的一些过滤器全都按顺序排着呢,我们自定义的AuthLoginFilter在最后一个。

    但是,再看看Tomcat中的第六个过滤器,居然也是我们自定义的AuthLoginFilter,它同时出现在TomcatShirofilters中,这样也就造成了前面提到的问题,Shiro在匹配到anon之后确实会将请求放行,但是在外层TomcatFilter中依旧被匹配上了,造成的现象好像是ShiroFilter配置规则失效了,其实这个问题跟Shiro并没有关系。

    问题的根源找到了,想要解决这个问题必须找到这个自定义的Filter何时被添加到Tomcat的过滤器执行链中以及其原因。

    追根溯源

    关于这个问题我找到了ServletContextInitializerBeans这个类中,它在Spring启动时就会初始化,在它的构造方法中做了很多初始化相关的操作。至于这一系列初始化流程就不得不提ServletContextInitializer相关知识点了,关于它的内容完全可以另开一片博客细说了。先看看ServletContextInitializerBeans的构造方法:

    @SafeVarargs
    public ServletContextInitializerBeans(ListableBeanFactory beanFactory,
            Class<? extends ServletContextInitializer>... initializerTypes) {
        this.initializers = new LinkedMultiValueMap<>();
        this.initializerTypes = (initializerTypes.length != 0) ? Arrays.asList(initializerTypes)
                : Collections.singletonList(ServletContextInitializer.class);
        // 上面提到的Filter正是在这个方法开始一步步被添加到ApplicationFilterChain中的
        addServletContextInitializerBeans(beanFactory);
        addAdaptableBeans(beanFactory);
        List<ServletContextInitializer> sortedInitializers = this.initializers.values().stream()
                .flatMap((value) -> value.stream().sorted(AnnotationAwareOrderComparator.INSTANCE))
                .collect(Collectors.toList());
        this.sortedList = Collections.unmodifiableList(sortedInitializers);
        logMappings(this.initializers);
    }
    

    上面提到的ApplicationFilterChain中的Filter正是在addServletContextInitializerBeans(beanFactory)这个方法开始一步步被添加到Filters中的,限于篇幅这里就看一下关键步骤。

    private void addServletContextInitializerBeans(ListableBeanFactory beanFactory) {
        for (Class<? extends ServletContextInitializer> initializerType : this.initializerTypes) {
            for (Entry<String, ? extends ServletContextInitializer> initializerBean : 
                    // 这里根据type获取Bean列表并遍历
                    getOrderedBeansOfType(beanFactory, initializerType)) {
                // 此处开始添加对应的ServletContextInitializer
                addServletContextInitializerBean(initializerBean.getKey(), initializerBean.getValue(), beanFactory);
            }
        }
    }
    

    addServletContextInitializerBeans(beanFactory)一路走下去会到达getOrderedBeansOfType()方法中,然后调用了beanFactorygetBeanNamesForType(),默认的实现在DefaultListableBeanFactory中,这里所贴前后删减掉了无关代码:

    private String[] doGetBeanNamesForType(ResolvableType type, boolean includeNonSingletons, boolean allowEagerInit) {
        List<String> result = new ArrayList<>();
        // 检查所有的Bean
        for (String beanName : this.beanDefinitionNames) {
            // 当这个Bean名称没有定义为其他bean的别名时,才进行匹配
            if (!isAlias(beanName)) {
                RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
                // 检查Bean的完整性,检测是否是抽象类,是否懒加载等等属性
                if (!mbd.isAbstract() && (allowEagerInit || (mbd.hasBeanClass() || !mbd.isLazyInit() || 
                        isAllowEagerClassLoading()) && !requiresEagerInitForType(mbd.getFactoryBeanName()))) {
                    // 匹配的Bean是否是FactoryBean,对于FactoryBean,需要匹配它创建的对象
                    boolean isFactoryBean = isFactoryBean(beanName, mbd);
                    BeanDefinitionHolder dbd = mbd.getDecoratedDefinition();
    				// 这里也是做完整性检查
                    boolean matchFound = (allowEagerInit || !isFactoryBean || (dbd != null && !mbd.isLazyInit())
                        || containsSingleton(beanName)) && (includeNonSingletons || 
                        (dbd != null ? mbd.isSingleton() : isSingleton(beanName))) && isTypeMatch(beanName, type);
                    if (!matchFound && isFactoryBean) {
                        // 对于FactoryBean,接下来尝试匹配FactoryBean实例本身
                        beanName = FACTORY_BEAN_PREFIX + beanName;
                        matchFound = (includeNonSingletons || mbd.isSingleton()) && isTypeMatch(beanName, type);
                    }
                    if (matchFound) {
                        result.add(beanName);
                    }
                }
            }
        }
        return StringUtils.toStringArray(result);
    }
    

    到这里就是关键所在了,它会根据目标类型调用isTypeMatch(beanName, type)匹配每一个被Spring接管的BeanisTypeMatch方法很长,这里就不贴了,有兴趣的可以自行去看看,它位于AbstractBeanFactory中。这里匹配的type就是ServletContextInitializerBeans遍历自构造方法中的initializerTypes列表。

    doGetBeanNamesForType出来后,再看这个方法:

    private void addServletContextInitializerBean(String beanName,
    		ServletContextInitializer initializer, ListableBeanFactory beanFactory) {
    	if (initializer instanceof ServletRegistrationBean) {
    		Servlet source = ((ServletRegistrationBean<?>) initializer).getServlet();
    		addServletContextInitializerBean(Servlet.class, beanName, initializer,
    				beanFactory, source);
    	}
    	else if (initializer instanceof FilterRegistrationBean) {
    		Filter source = ((FilterRegistrationBean<?>) initializer).getFilter();
    		addServletContextInitializerBean(Filter.class, beanName, initializer,
    				beanFactory, source);
    	}
    	else if (initializer instanceof DelegatingFilterProxyRegistrationBean) {
    		String source = ((DelegatingFilterProxyRegistrationBean) initializer)
    				.getTargetBeanName();
    		addServletContextInitializerBean(Filter.class, beanName, initializer,
    				beanFactory, source);
    	}
    	else if (initializer instanceof ServletListenerRegistrationBean) {
    		EventListener source = ((ServletListenerRegistrationBean<?>) initializer)
    				.getListener();
    		addServletContextInitializerBean(EventListener.class, beanName, initializer,
    				beanFactory, source);
    	}
    	else {
    		addServletContextInitializerBean(ServletContextInitializer.class, beanName,
    				initializer, beanFactory, initializer);
    	}
    }
    

    前面两个配置过FilterServlet的应该很熟悉,Spring中添加自定义Filter经常这么用,添加Servlet同理:

    @Bean
    public FilterRegistrationBean xssFilterRegistration() {
    	FilterRegistrationBean registration = new FilterRegistrationBean();
    	registration.setDispatcherTypes(DispatcherType.REQUEST);
    	registration.setFilter(new XxxFilter());
    	registration.addUrlPatterns("/*");
    	registration.setName("xxxFilter");
    	return registration;
    }
    

    这样Spring就会将其添加到过滤器执行链中,当然这只是添加Filter的众多方式之一。

    解决方案

    那么问题的根源找到了,被Spring接管的Bean中所有的Filter都会被添加到ApplicationFilterChain,那我不让Spring接管我的AuthLoginFilter不就行了。如何做?配置的时候直接new出来,还记得前面的那两行代码吗:

    // 这里注释的一行是我这次踩的一个小坑,我一开始按下面这么配置产生了一个我意料之外的问题
    // filters.put("authLogin", authLoginFilter());
    // 正确的配置是需要我们自己new出来,不能将这个Filter交给Spring管理
    filters.put("authLogin", new AuthLoginFilter(500, "未登录或登录超时"));
    

    OK,问题解决,就是这么简单。但就是这么小小的一个问题,在不清楚问题产生的原因的情况下,根本想不到是Spring接管Filter造成的,了解了底层,才能更好的排查问题。


    尾巴

    • Shiro中自定义Filter仅需要继承AccessControlFilter类后实现参与过滤的两个方法,再将其配置到ShiroFilterFactoryBean中即可。
    • 需要注意的点是,因为Spring的初始化机制,我们自定义的Filter如果被Spring接管,那么会被Spring添加到ApplicationFilterChain中,导致这个自定义过滤器会被重复执行,也就是无论Shiro中的过滤器过滤结果如何,最后依旧会走到被添加到ApplicationFilterChain中的自定义过滤器。
    • 解决这个问题的方法非常简单,不让Spring接管我们的Filter,直接new出来配置到Shiro即可。
    • 码海无涯,不进则退,日积跬步,以至千里。

    Shiro系列博客项目源代码地址:

    Gitee:https://gitee.com/guitu18/ShiroDemo

    GitHub:https://github.com/guitu18/ShiroDemo


  • 相关阅读:
    vba中数据类型
    Excel统计函数COUNTIF()的常规用法介绍
    分类求和
    在每个sheet的相同位置写入相同的值
    Timer函数
    数组总结
    如何制作本地yum源
    HDFS的优缺点
    HDFS安全模式
    HDFS的HA(高可用)
  • 原文地址:https://www.cnblogs.com/guitu18/p/12163872.html
Copyright © 2011-2022 走看看