我们都知道shiro是个认证权限框架,除了登录、退出逻辑我们需要侵入项目代码之外,验证用户是否已经登录、是否拥有权限的代码其实都是过滤器来完成的(包含注解),可以这么说,shiro其实就是一个过滤器链集合。当运行web应用程序时,Shiro将创建一些有用的默认过滤器实例,并使它们在[main]部分自动可用。您可以像配置任何其他bean一样在main中配置它们,并在链定义中引用它们。
1、filter
默认筛选器实例由DefaultFilter enum中定义,enum s name字段是可用于配置的名称。
于是我看了一下DefaultFilter
的源码:
1 | public enum DefaultFilter { |
1 | /admin/**=anon :无参,表示可匿名访问 |
终于知道我们常用的anon、authc、perms、roles、user过滤器是哪里来的了!这些过滤器我们都是可以直接使用的。但你要弄清楚这些默认过滤器,你还不得不去深入了解一下shiro更底层为我们提供的过滤器,基本我们的这些默认过滤器都是通过继承这几个底层过滤器演变而来的。
1、AbstractFilter
这个过滤器还得说说,shiro最底层的抽象过滤器,虽然我们极少直接继承它,它通过实现Filter
获得过滤器的特性。子类初始化逻辑应通过重onFilterConfigSet()
模板方法来执行。FilterChain执行逻辑javax.servlet.ServletRequest,javax.servlet.ServletResponse,javax.servlet.FilterChain
方法留给子类。保存了Servlet
的相关配置。
完成一些过滤器基本初始化操作,FilterConfig
:过滤器配置对象,用于servlet容器在初始化期间将信息传递给其他过滤器。
2、NameableFilter
命名过滤器,给过滤器定义名称,保存了该Filter的名称!也是比较基层的过滤器了,未拓展其他功能,我们很少会直接继承这个过滤器。为重写doFilter方法。
3、OncePerRequestFilter
保证了在每次处理 http 请求时,保证该 filter 只会执行一次。 重写doFilter方法,保证每个servlet方法只会被过滤一次。可以看到doFilter方法中,第一行代码就是String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
然后通过request.getAttribute(alreadyFilteredAttributeName) != null
来判断过滤器是否已经被调用过,从而保证过滤器不会被重复调用。
进入方法之前,先标记alreadyFilteredAttributeName
为True,抽象doFilterInternal
方法执行之后再remove掉alreadyFilteredAttributeName
。
1 | public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) |
所以OncePerRequestFilter过滤器保证只会被一次调用的功能,提供了抽象方法doFilterInternal
让后面的过滤器可以重写,执行真正的过滤器处理逻辑。
1 | protected abstract void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) |
这个过滤器我们已经可以开始在我们的项目继承使用,比如拦截用户请求,判断用户是否已经登录(携带token或cookie信息),如果未登录则返回Json数据告知未登录!
比如:
开源mblog博客项目中,过滤器就是继承OncePerRequestFilter。
1 | public class AuthenticatedFilter extends OncePerRequestFilter { |
未登录情况,ajax请求过滤器返回您还没有登录!
提示,web请求则返回一段js代码,前端渲染会跳出一个登陆窗口,这也就是未什么大家常遇到的点击登录,当前跳出一个登陆弹窗的一种实现方式!
效果:
4、AdviceFilter(doFilterInternal分化成了切面编程)
看到Advice,很自然想到切面环绕编程,一般有pre、post、after几个方法。所以这个AdviceFilter过滤器就是提供了和AOP相似的切面功能。
继承OncePerRequestFilter过滤器重写doFilterInternal方法,我们可以先看看:
1 | public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) |
于是,我们从OncePerRequestFilter的一个doFilterInternal分化成了切面编程,更容易前后控制执行逻辑。所以如果继承AdviceFilter时候,我们可以重写preHandle方法,判断用户是否满足已登录或者其他业务逻辑,返回false时候表示不通过过滤器。
5、PathMatchingFilter (出现onPreHandle)
请求路径匹配过滤器,通过匹配请求url,判断请求是否需要过滤,如果url未在需要过滤的集合内,则跳过,否则进入isFilterChainContinued
的onPreHandle方法。
我们可以看下代码:
1 | // 路径配置过滤器则放行,不匹配则启用过滤器。放行之后不执行过滤逻辑所以不登录啥的都没问题 |
1 | private boolean isFilterChainContinued(ServletRequest request, ServletResponse response, |
从上面3个步骤中可以看到,PathMatchingFilter提供的功能是:自定义匹配url,匹配上的请求最终跳转到onPreHandle
方法。
这个过滤器为后面的常用过滤器提供的基础,比如我们在config中配置如下
1 | /login = anon |
拦截/login请求,经过AnonymousFilter过滤器,我们可以看下
- org.apache.shiro.web.filter.authc.AnonymousFilter
1 | public class AnonymousFilter extends PathMatchingFilter { |
AnonymousFilter重写了onPreHandle方法,只不过直接返回了true,说明拦截的链接可以直接通过,不需要其他拦截逻辑。
而authc->FormAuthenticationFilter也是间接继承了PathMatchingFilter。
1 | public class FormAuthenticationFilter extends AuthenticatingFilter |
所以,需要拦截某个链接进行业务逻辑过滤的可以继承PathMatchingFilter方法拓展哈。
6、AccessControlFilter
访问控制过滤器。继承PathMatchingFilter过滤器,重写onPreHandle方法,又分出了两个抽象方法来控制
1 | public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { |
所以,我们现在可以通过重写这个抽象两个方法来控制过滤逻辑。另外多提供了3个方法,方便后面的过滤器使用。
1 | //子类在需要登录重定向时使用的便捷方法。 |
其中redirectToLogin提供了调整到登录页面的逻辑与实现,为后面的过滤器发现未登录跳转到登录页面提供了基础。
这个过滤器,我们可以灵活运用。
7、AuthenticationFilter
继承AccessControlFilter,重写了isAccessAllowed方法,通过判断用户是否已经完成登录来判断用户是否允许继续后面的逻辑判断。这里可以看出,从这个过滤器开始,后续的判断会与用户的登录状态相关,直接继承这些过滤器,我们不需要再自己手动去判断用户是否已经登录。并且提供了登录成功之后跳转的方法。
1 | public abstract class AuthenticationFilter extends AccessControlFilter { |
8、AuthenticatingFilter
继承AuthenticationFilter,提供了自动登录、是否登录请求等方法。
1 | /** |
- executeLogin 执行登录
- onLoginSuccess 登录成功跳转
- onLoginFailure 登录失败跳转
- createToken 创建登录的身份token
- isAccessAllowed 是否允许被访问
- isLoginRequest 是否登录请求
这个方法提供了自动登录的课程,比如我们获取到token之后实行自动登录,这场景还是很场景的。
比如在开源项目renren-fast中,就是这样处理的:
1 | public class OAuth2Filter extends AuthenticatingFilter { |
在onAccessDenied
方法校验通过之后执行executeLogin
方法完成自动登录!
9、FormAuthenticationFilter
基于form表单的账号密码自动登录的过滤器,我们只需要看这个方法就明白,和renren-fast的实现相似:
1 | public class FormAuthenticationFilter extends AuthenticatingFilter { |
onAccessDenied调用executeLogin方法。默认的token是UsernamepasswordToken。
10、AbstractShiroFilter
10.1 主filter
shrio 只会创建一个主 Filter,嵌入到Servlet
中。然后其它的独立 Filter 会被添加到主 Filter 里,这种设计可以简化对Servlet
的修改,并且可以自定义管理多个独立 Filter。主 Filter 会以 Chain 的方式来组织管理,如下图所示。主 Filter 根据请求的路径不同,选择不同的 FilterChain 来处理。
AbstractShiroFilter
作为实现主 Filter 的核心类,需要详细讲解。它在处理请求时,会先创建Subject
实例,然后开辟新线程来处理。
1 | public abstract class AbstractShiroFilter extends OncePerRequestFilter { |
在创建线程的时候,shiro 做了一些初始化,它会绑定当前的Subject
和SecurityManager
,存储到线程上下文。结合之前的文章,就可以看到线程安全的使用。
继续看看executeChain
的代码,可以看到 shiro 会有一个根据请求选择FilterChain
的过程。
1 | public abstract class AbstractShiroFilter extends OncePerRequestFilter { |
FilterChain 匹配原理
shiro 支持根据请求来选择对应的FilterChain
,这个功能由FilterChainResolver
接口定义。
1 | public interface FilterChainResolver { |
它有两个子类,PathMatchingFilterChainResolver
和SimpleFilterChainResolver
,都是根据请求路径来判断的。两者的实现没什么本质区别,都是使用Map
保存了匹配规则和对应的FilterChain
,只不过前者将路径匹配和FilterChain
管理分离开了。
SimpleFilterChainResolver 类
首先来讲讲SimpleFilterChainResolver
的原理,它非常简单
1 | class SimpleFilterChainResolver implements FilterChainResolver { |
SimpleFilterChain
包含了两部分的Filter
,原始的 Serlvet FilterChain
,和上述Filter
列表。它被调用时,会先执行Filter
列表,然后再执行Serlvet FilterChain
。
1 | class SimpleFilterChain implements FilterChain { |
PathMatchingFilterChainResolver类
PathMatchingFilterChainResolver
是 shiro 默认的实现类,它只负责匹配的功能,管理FilterChain
的功能被分离开来了,使得我们可以自由定义FilterChain
。
管理FilterChain
的功能由FilterChainManager
负责,目前只有DefaultFilterChainManager
一种实现。它的实现也非常简单,我们通过设置filters
和filterChains
的值,就可以完成配置。
1 | public class DefaultFilterChainManager implements FilterChainManager { |
10.2 独立 Filter
我们再来看看独立Filter 的类图,依次从上到下看。
AdviceFilter
增加了preHandle
方法,子类可以通过实现它,来控制是否继续执行 FilterChain
。
PathMatchingFilter
增加了请求路径匹配的功能。
AccessControlFilter
作为身份和权限认证的父类,提供了执行失败后的回调函数。
下面的类可以分为三块,
AnonymousFilter
,表示允许匿名访问AuthenticationFilter
及其子类,表示身份认证方面的AuthorizationFilter
及其子类,表示权限检查方面的
匿名访问 Filter
AnonymousFilter
允许匿名访问,它复写了onPreHandle
方法,总是放回 true,表示运行通过。
1 | public class AnonymousFilter extends PathMatchingFilter { |
AccessControlFilter 类
AccessControlFilter
实现了onPreHandle
方法,并且还定义了isAccessAllowed
和onAccessDenied
的方法。子类需要实现isAccessAllowed
来负责用户身份认证或者权限检查的逻辑。还需要实现onAccessDenied
方法来执行失败后的处理逻辑。
AccessControlFilter
会通过两个方法处理的接口,来决定是否要继续执行 filter chain。
身份认证 Filter
身份认证的 Filter 类也比较复杂,从上到下依次介绍:
AuthenticationFilter
实现了isAccessAllowed
方法,它会判断当前的Subject
是否对已经认证过了。
AuthenticatingFilter
提供了登录的方法。
FormAuthenticationFilter
提供了表单登录的方法。当用户请求登录地址时(默认为login.jsp
),它会自动完成登录操作,并且实现跳转页面操作。我们不用再去定义处理登录的 controller。
BasicHttpAuthenticationFilter
提供了 http basic 验证方法。
我们也可以自定义 Filter,只需要继承AuthenticationFilter
类,然后实现onAccessDenied
方法即可。比如我们在实现 restful 风格的 api 时,当发现用户没登录时,只需要返回 json。
1 | public class RestfulFilter extends AuthenticationFilter { |
权限检查 Filter
权限认证的 Filter 类会相对简单一些,首先看看父类AuthorizationFilter
。它实现了onAccessDenied
方法,当权限检查没通过时,会被调用。如果用户没有认证通过的话,AuthorizationFilter
会返回 302 响应,跳转到登录地址。如果是权限校检没通过,那么会返回 401 响应。
AuthorizationFilter
还有很多子类,每个子类都会实现各自的权限校检逻辑。下面表列举了各种子类
类名 | 检查逻辑 |
---|---|
HostFilter | 根据用户的ip来判断,是否允许通过 |
PermissionsAuthorizationFilter | 根据用户的权限来判断,是否允许通过 |
PortFilter | 根据用户的端口号来判断,是否允许通过 |
HttpMethodPermissionFilter | 根据用户的请求方法来判断,是否允许通过 |
RolesAuthorizationFilter | 根据用户的角色来判断,是否允许通过 |
SslFilter | 用户只有https请求,才会允许通过 |
10.3 Filter 创建
ShiroFilterFactoryBean
负责创建并且始化主 Filter,我们在使用 spring 配置时,都是修改它的参数来完成。下面是它的createInstance
方法,
1 | public class ShiroFilterFactoryBean implements FactoryBean, BeanPostProcessor { |
上述返回了一个SpringShiroFilter,它非常简单,只是简单的继承了AbstractShiroFilter
。创建过程中最为核心的部分是FilterChainManager
的创建,通过设置filters
和filterChainDefinitionMap
哈希表,完成配置化。
1 | public class ShiroFilterFactoryBean implements FactoryBean, BeanPostProcessor { |
2、shiro 注解
RequiresAuthentication:使用该注解标注的类,实例,方法在访问或调用时,当前Subject必须在当前session中已经过认证。
RequiresGuest:使用该注解标注的类,实例,方法在访问或调用时,当前Subject可以是“Guest”身份,不能经过认证或者在原先的session中存在记录。
RequiresPermissions:当前Subject需要拥有某些特定的权限时,才能执行被该注解标注的方法。如果当前Subject不具有这样的权限,则方法不会被执行。
RequiresRoles:当前Subject必须拥有所有指定的角色时,才能访问被该注解标注的方法。如果当天Subject不同时拥有所有指定角色,则方法不会执行还会抛出AuthorizationException异常。
RequiresUser:当前Subject必须是应用的用户,才能访问或调用被该注解标注的类,实例,方法。
1、开启shiro注解
1 |
|
2、StaticMethodMatcherPointcutAdvisor 静态普通方法名匹配切面
1 | public class AuthorizationAttributeSourceAdvisor extends StaticMethodMatcherPointcutAdvisor { |
3、注解类处理器对相应注解做解析
1 | public AopAllianceAnnotationsAuthorizingMethodInterceptor() { |
以PermissionAnnotationMethodInterceptor为例:
1 | public PermissionAnnotationMethodInterceptor(AnnotationResolver resolver) { |
调用父类构造函数并且第一个参数 字面意思可以看出 是对 权限注解的处理类,父类为AuthorizingAnnotationMethodInterceptor,一般我们知道AOP 执行的方法为
invoke() 看到此类中的invoke方法
1 | public Object invoke(MethodInvocation methodInvocation) throws Throwable { |
权限检查
1 | public void assertAuthorized(Annotation a) throws AuthorizationException { |
1 | public void checkPermission(String permission) throws AuthorizationException { |
继续看securityManager的checkPermission方法,中间类似方法的调用 最后在ModularRealmAuthorizer中
1 | public void checkPermission(PrincipalCollection principals, String permission) throws AuthorizationException { |
if语句中判断是否满足条件
1 | public boolean isPermitted(PrincipalCollection principals, String permission) { |
最后调到AuthorizingRealm的isPermitted方法
1 | public boolean isPermitted(PrincipalCollection principals, Permission permission) { |
getAuthorizationInfo方法为
1 | protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) { |
上面同样是从缓存中取,直接看下面 ,doGetAuthorizationInfo()方法 有没有很眼熟,没错 就是我们自定义realm里面可以拓展的方法,一般就是从数据库获取配置的
权限字符串然后返回过来了
最后就是比较权限字符串是否包含权限注解中的字符串了
1 | protected boolean isPermitted(Permission permission, AuthorizationInfo info) { |
当然没满足就会抛出没有权限的异常了。
3、总结
请求进来之后先进行过滤,之后如果存在注解的方法springAOP将对其进行权限检查。
参考
1 | https://segmentfault.com/a/1190000022631899 |
- 本文作者: 初心
- 本文链接: http://funzzz.fun/2021/02/03/Apache Shiro --- 9 filter and annotation/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!