在使用 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
再好不过了。
还是以第一篇的Demo为例,项目地址在文章尾部有放上,本篇在之前的代码上继续添加功能。
首发地址: https://www.guitu18.com/post/2020/01/06/64.html
在实现自定义Filter之前,我们先看看这个类: org.apache.shiro.web.filter.AccessControlFilter
,点开它的子类,发现子类全部都是 org.apache.shiro.web.filter.authc
和 org.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的,第五个是 Shiro
的 ShiroFilterFactoryBean
,它的内部也维护了一个 filters
,用来保存 Shiro
内置的一些过滤器和我们自定义的过滤器, Tomcat
所维护的 filters
和 Shiro
维护的 filters
是一个父子层级的关系 , Shiro
中的 ShiroFilterFactoryBean
仅仅只是 Tomcat
里 filters
中的一员。点开看 ShiroFilterFactoryBean
查看,果然 Shiro
内置的一些过滤器全都按顺序排着呢,我们自定义的 AuthLoginFilter
在最后一个。
但是,再看看 Tomcat
中的第六个过滤器,居然也是我们自定义的 AuthLoginFilter
,它同时出现在 Tomcat
和 Shiro
的 filters
中,这样也就造成了前面提到的问题, Shiro
在匹配到 anon
之后确实会将请求放行,但是在外层 Tomcat
的 Filter
中依旧被匹配上了,造成的现象好像是 Shiro
的 Filter
配置规则失效了,其实这个问题跟 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()
方法中,然后调用了 beanFactory
的 getBeanNamesForType()
,默认的实现在 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接管的 Bean
, isTypeMatch
方法很长,这里就不贴了,有兴趣的可以自行去看看,它位于 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); } }
前面两个配置过 Filter
和 Servlet
的应该很熟悉, 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