明确需求
在使用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.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