Shiro rights management framework (five): Custom Filter Implementation and Troubleshooting record

Clear demand

Use Shirotime, authentication failures are generally returns an error page or log on to the front pages, especially back-office systems, particularly with this model. But now more and more projects using the front and rear ends tend to develop separate manner, which requires a response time Jsondata to the distal end of the tip and then accordingly based on the status code. Shiro framework can then return when the authentication failure of direct Jsondata it? The answer of course is yes.

In fact, Shirothe custom filters especially powerful, you can achieve a lot of useful features, return to the front-end Jsondata overemphasized. Usually we did not go to pay attention to it because of Shirothe built-in filter function has been more about the whole, the background system permissions control basically have Shirobuilt some of the filters can be achieved, and here again labeled this figure.

This was the third time I posted this picture of

Related documents address: http: //shiro.apache.org/web.html#default-filters

My most recent project is the need to provide a functional interface to a mobile phone APP, user login needs to be done, Sessionpersistence and Sessionsharing, but do not require fine-grained access control. Faced with this demand my first thought is integrated Shiro, and Sessionthe persistence and shared in Shiroseries The second has been talked about, then this way with about Shirocustom filters. There is no need to provide fine-grained access control, only need to login authentication, but also a response to the front-end authentication fails after Jsonthe data, then use the custom Filtercould not be better.

Custom Filter

Or in the first chapter of the Demo, for example, the project has put the tail address in the article herein, this continues to add functionality in the previous code.

Starting address: https://www.guitu18.com/post/2020/01/06/64.html

Before you implement a custom Filter, we take a look at this class: org.apache.shiro.web.filter.AccessControlFilterpoint to open its sub-categories, sub-categories are all found org.apache.shiro.web.filter.authcand org.apache.shiro.web.filter.authzthese two packages, mostly inherited AccessControlFilterthis class. These subclasses of class name is not very familiar, look at the map goes three times I posted above, most of them in here yet.

It seems AccessControlFilterthat this class is closely related with the filter Shiro permission, then take a look at its architecture:

It's top-level parent class is javax.servlet.Filterin front of us have said, Shiro all permissions are based filtering Filterto achieve . Custom Filteralso need to realize AccessControlFilter, here we add a login authentication filter, as follows:

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

Custom filters written, and now need to give it to Shiro management:

@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;
}

So Shiro add a custom filter is complete. Custom Filtercan add more to achieve different needs, you just need to filtersplay a good name filter will putgo in and filterChainMapadd a mapping filter aliases and paths can use this filter up. One thing to note is that the filter is matched in sequence from front to back , so that a large range of the path on the back should putgo.

Filter here custom function has been achieved, followed by investigation record pit mining, of interest may not be skipped.

Troubleshooting

First half describes how to use Shirocustom Filterfunctions to achieve filtering in Shiroconfiguration code, I put a small dip a step on the configuration, if we will custom Filter Spring to the management, will produce some unexpected problems . Indeed, usually in the Spring project, doing the configuration, we will default Spring Bean handed over to management, are generally not a problem, but this is not the same, look at the following code:

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即可。
  • Code sea knows no boundaries, fall behind, day short step, a thousand miles.

Shiro series of blog project source address:

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

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


Guess you like

Origin www.cnblogs.com/guitu18/p/12163872.html