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.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
Shiro权限管理框架(五):自定义Filter实现及其问题排查记录的更多相关文章
- Shiro权限管理框架(一):Shiro的基本使用
首发地址:https://www.guitu18.com/post/2019/07/26/43.html 核心概念 Apache Shiro是一个强大且易用的Java安全框架,执行身份验证.授权.密码 ...
- Shiro权限管理框架(二):Shiro结合Redis实现分布式环境下的Session共享
首发地址:https://www.guitu18.com/post/2019/07/28/44.html 本篇是Shiro系列第二篇,使用Shiro基于Redis实现分布式环境下的Session共享. ...
- Shiro权限管理框架(三):Shiro中权限过滤器的初始化流程和实现原理
本篇是Shiro系列第三篇,Shiro中的过滤器初始化流程和实现原理.Shiro基于URL的权限控制是通过Filter实现的,本篇从我们注入的ShiroFilterFactoryBean开始入手,翻看 ...
- Shiro权限管理框架(四):深入分析Shiro中的Session管理
其实关于Shiro的一些学习笔记很早就该写了,因为懒癌和拖延症晚期一直没有落实,直到今天公司的一个项目碰到了在集群环境的单点登录频繁掉线的问题,为了解决这个问题,Shiro相关的文档和教程没少翻.最后 ...
- Shiro权限管理框架
一.Shiro介绍 Apache Shiro 是Java 的一个安全框架.Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE 环境,也可以用在JavaEE 环境.Shiro 可以 ...
- Shiro权限管理框架详解
1 权限管理1.1 什么是权限管理 基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被 ...
- shiro 返回json字符串 + 自定义filter
前言: 在前后端分离的项目中, 在使用shiro的时候, 我们绝大部分时候, 并不想让浏览器跳转到那个页面去, 而是告诉前端, 你没有登录, 或者没有访问权限. 那这时候, 我们就需要返回json字符 ...
- shiro权限管理框架与springmvc整合
shiro是apache下的一个项目,和spring security类似,用于用户权限的管理‘ 但从易用性和学习成本上考虑,shiro更具优势,同时shiro支持和很多接口集成 用户及权限管理是众多 ...
- Shiro 权限管理框架
一.什么是Shiro Apache Shiro是一个强大易用的java安全框架,提供认证.授权.加密和会话管理等功能 · 认证:用户身份识别,俗称“登录”: · 授权:访问控制 · 密码加密:保护或隐 ...
随机推荐
- 高级PHP开发:利用PHPEMS搭建在线考试平台
今天给大家分享一个小技巧,就是利用PHP ems搭建在线考试平台:希望能给你给予帮助: 在给大家分享之前,这里推荐下我自己建的PHP开发-VIP资料出售平台 :638965404,不管你是小白还是大牛 ...
- NIO 中文乱码自我解决的简单DEMO
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStrea ...
- P1109 桃花岛
题目描述 不是任何人都可以进入桃花岛的,黄药师最讨厌象郭靖一样呆头呆脑的人.所以,他在桃花岛的唯一入口处修了一条小路,这条小路全部用正方形瓷砖铺设而成.有的瓷砖可以踩,我们认为是安全的,而有的瓷砖一踩 ...
- spring json 返回中文乱码
如前台显示的json数据中的中文为???,则可尝试以下方法. 方法一(推荐):在@RequestMapping中添加 produces={"text/html;charset=UTF-8; ...
- urlencode()与urldecode()
urlencode()函数原理就是首先把中文字符转换为十六进制,然后在每个字符前面加一个标识符%. urldecode()函数与urlencode()函数原理相反,用于解码已编码的 URL 字符串,其 ...
- SVN提示update更新成功,但是本地文件却没有更新
问题描述:将仓库的最新版本代码check out到本地后,然后最某个文件做了修改,保存后想通过svn的update来重新得到最新的版本,发现失效. 原因:经过多方查找原因,主要看了以下两篇文档 htt ...
- P1089 过独木桥
题目描述 今年的 CSP-J/S 比赛马上就要开始了,代码决定的 N 位女学生排队去参加比赛. 期间他们遇到了代码决定的 M 位男生组成的男生队伍. 他们堵在了一座独木桥前.但是独木桥每次只能过一个人 ...
- P1053 第K小的取法
题目描述 给定一个含n个数的数组.现在从中取出一些数.并把这些数相加得出一个和,如果有多种取法的和相同,则视为多种取法.求所有取法对应的和中第K小的和. 输入格式 第一行包括两个正整数n(n<= ...
- 给培训学校讲解ORM框架的课件
导读:这是我给某培训学校培训.net程序员所设计的课件,他们普遍反映太难了,是这样吗?
- 2018-2-13-win10-uwp-判断文件存在
title author date CreateTime categories win10 uwp 判断文件存在 lindexi 2018-2-13 17:23:3 +0800 2018-2-13 1 ...