拦截器
- NameableFilter: 给Filter起了名字,如果没有设置默认就是FilterName;之前的authc就是,当我们组装拦截器链时会根据这个名字找到相应的拦截器实例。
- OncePerRequestFilter:防止多次执行Filter,也就是说一次请求只走一次拦截器链(比如一个非法请求过了一次拦截器后被强制转到登录请求,则此登录请求直接映射到处理类上,而不会再走一次拦截器链判断是否有权限或已登录。另外提供enable属性,默认true表示开启该拦截器实例,如果不想让某个拦截器工作,可以直接设置为false。
- ShiroFilter:是整个Shiro的入口点,用于拦截需要安全控制的请求进行处理。在其中设置loginUrl后会自动设置到所有的AccessControllerFilter。
- AdviceFilter:提供了aop风格的支持。
/*
* 调用下面方法的总方法
*/
public void doFilterInternal(ServletRequest request, ServletResponse response,
FilterChain chain) throws ServletException, IOException {
Exception exception = null;
try {
//返回true则继续执行过滤链,返回false则执行一些必要的善后即可,
//如跳转到登录页面并返回false,则不需要执行过滤链了
boolean continueChain = preHandle(request, response); //调用onPreHandle()
if (continueChain) {
//chain收集了为该请求配置的all过滤器,该方法将继续执行后续过滤器
executeChain(request, response, chain);
}
postHandle(request, response);
} catch (Exception e) {
exception = e;
} finally {
cleanup(request, response, exception);
}
}
//在过滤链执行前调用,返回true则继续过滤链,否则中断后续的过滤链直接返回。
//进行预处理,比如身份验证、授权
boolean preHandle(ServletRequest request, ServletResponse response) throws Exception;
//在过滤链执行后调用,如记录执行时间。
void postHandle(ServletRequest request, ServletResponse response) throws Exception;
//类似于aop中的后置最终增强,即不管有没有异常都会执行
//如清理资源(解除Subject与线程的绑定等)
void afterCompletion(ServletRequest request, ServletResponse response, Exception exception)
throws Exception;
- PathMatchingFilter:提供了基于Ant风格的请求路径匹配功能及拦截器参数解析的功能,如”roles[admin,user]”自动根据“,”分割解析到一个路径参数配置并绑定到相应的路径。
//用于path与请求路径进行匹配,匹配则返回true
boolean pathsMatch(String path, ServletRequest request)
//在preHandle中,当pathsMatch匹配一个路径后,会调用onPreHandle方法并将路径绑定参数配置
//传给mappedValue,
//然后可以在该方法中进行一些验证(如权限验证),验证失败返回false中断流程,默认返回true;
//也即是子类可以只实现onPreHandle即可,无需实现preHandle()。
//如果没有path与请求路径匹配,默认是通过的(即preHandle返回true)。
boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue)
throws Exception
- AccessControllerFilter:提供了访问控制的基础功能,比如什么情况允许访问或什么情况拒绝访问。
//何时允许访问;mappedValue就是ini配置文件中[urls]中拦截器参数部分,允许则返回true;
//返回true则不会再执行onAccessDenied。
abstract boolean isAccessAllowed(ServletRequest request, ServletResponse response,
Object mappedValue) throws Exception;
//何时拒绝访问;返回值表示是否需要继续处理,true表示还要继续处理,可能会返回默认地址;
//否则直接返回即可(如,发现用户未登录故拒绝访问当前页面,但已重定向到登录页面,
//故不需要继续处理了,直接返回即可)。
boolean onAccessDenied(ServletRequest request, ServletResponse response,
Object mappedValue) throws Exception;
//同上,onPreHandle会自动调用这俩方法决定是否继续处理。
abstract boolean onAccessDenied(ServletRequest request, ServletResponse response)
throws Exception;
boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return isAccessAllowed(request, response, mappedValue)
|| onAccessDenied(request, response, mappedValue);
}
- 还提供了处理登录成功后或重定向到上一个请求的方法:
void setLoginUrl(String loginUrl) //身份验证时使用,默认/login.jsp
String getLoginUrl()
Subject getSubject(ServletRequest request, ServletResponse response) //获取Subject实例
boolean isLoginRequest(ServletRequest request, ServletResponse response)
//当前请求是否是登录请求
void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response)
throws IOException; //将当前请求保存起来并重定向到登录页面
void saveRequest(ServletRequest request) //将请求保存起来,如登录成功后再重定向回该请求
void redirectToLogin(ServletRequest request, ServletResponse response) //重定向到登录页面
如果我们想控制访问可以继承AccessControllerFilter,要添加一些通用数据可以继承PathMatchingFilter。
AuthenticationFilter:继承自AccessControllerFilter,又有AuthenticatingFilter子类,然后是基于表单的拦截器FormAuthenticationFilter。
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response,
Object mappedValue) {
//判断是否已身份验证
Subject subject = getSubject(request, response);
return subject.isAuthenticated();
}
//onAccessDenied()只让最底层的类实现,其他父类只提供abstract接口。
- 以上isAccessAllowed()的bug是,如果用户已登录但还是请求再次登录,那么在执行此方法时将会被直接返回,最后分发到/login的controller进行失败处理,从而重新回到登录页面,导致死循环。因此,最好再加上判断请求是否为登录请求,不是才返回true。
代码示例:ideaProjects/shiro-chapter22/jcaptcha/MyFormAuthenticationFilter
AuthorizationFilter:同样继承自AccessControllerFilter。
FormAuthenticationFilter:AccessControllerFilter间接子类。
拦截器调用链
- 从高到低:
- AbstractShiroFilter.executeChain(): 开始过滤链。
- PathMatchingChainResolver.getExecutionChain(): 获取过滤链,如,/** = kickout,user,sysUser配置了三个过滤链;doFilter(): 执行第一个拦截器。
- doFilterInternal():由doFilter()调用;调用preHandle(),返回true则调用executeChain()继续执行下一拦截器。
- AdviceFilter.executeChain():执行下一拦截器。
- doFilter():开始执行。
FormAuthenticationFilter调用链
- 扩展上面第3步,调用链从高级到低级:
- PathMatchingFilter.preHandle():访问某url,ShiroFilter拦截该url,调用该方法判断它指定了哪个Filter,然后这个Filter实现了哪些方法就执行哪些方法。
- AccessControlFilter.onPreHandle():如果实现了该方法的话,它将会调用isAccessAllow()和onAccessDenied(),但通常是直接让前者返回false,主要的验证逻辑放在后者实现;或者主要研验证逻辑放在前者实现,后者做响应的校验失败处理,比如把失败信息存入request。
- FormAuthenticationFilter.onAccessDenied():在需要权限的方法执行前调用,而不是被拒绝访问后才执行,不要望文生义。比如判断用户是否已登录,未登录则重定向至登录页面,正在登录则调用以下login()接收表单验证,已登录则继续过滤链,最终访问到目标方法。
- DelegatingSubject.login()
- ModularRealmAuthenticator.doAuthenticate()
- UserRealm.doGetAuthenticationInfo(),若为自定义Realm而是使用ini配置文件准备了用户库的话,那就是调用那个Realm了。
- 也就是说FormAuthenticationFilter是会调用login()的,而login()又会调用Realm,所以FormAuthenticationFilter的登录验证是通过Realm实现的。
拦截器链
- Shiro通过ProxiedFilterChain对Servlet容器的FilterChain进行了代理,它会先执行Shiro自己的Filter链,再执行Servlet容器原始的Filter链。
//传入原始的chain得到一个代理的chain。
FilterChain getChain(ServletRequest request, ServletResponse response,
FilterChain originalChain);
- DefaultFilterChainManager:FilterChainManager实现类,维护着拦截器链。默认包含如下拦截器:
public enum DefaultFilter {
anon(AnonymousFilter.class),
authc(FormAuthenticationFilter.class),
authcBasic(BasicHttpAuthenticationFilter.class),
logout(LogoutFilter.class),
noSessionCreation(NoSessionCreationFilter.class),
perms(PermissionsAuthorizationFilter.class),
port(PortFilter.class),
rest(HttpMethodPermissionFilter.class),
roles(RolesAuthorizationFilter.class),
ssl(SslFilter.class),
user(UserFilter.class);
}
- 另外还提供了一个org.apache.shiro.web.filter.authz.HostFilter,即主机拦截器,其提供了属性authorizedIps:已授权的ip,deniedIps:拒绝的ip。不过目前尚未完全实现,不可用。
- 如果不想启用某个拦截器可以直接在ini禁用:
perms.enabled=false
FilterChainResolver
- FilterChainResolver:将url与同名拦截器对应起来。Shiro提供了PathMatchingFilterChainResolver实现类,其根据[urls]中的url解析得到相应的拦截器链(其内部通过FilterChainManager维护着拦截器链)。因此可自定义FilterChainResolver来动态实现url-拦截器的注册。
- 如果要注册自定义FilterChainResolver,IniSecurityManagerFactory或WebIniSecurityManagerFactory会自动扫描ini配置文件中的[filters]/[main]部分并注册这些拦截器到DefaultFilterChainManager。
/**
* 通过实现WebEnvironment接口完成自定义FilterChainResolver
*/
public class MyIniWebEnvironment extends IniWebEnvironment {
@Override
protected FilterChainResolver createFilterChainResolver() {
//1、创建FilterChainResolver
PathMatchingFilterChainResolver filterChainResolver =new PathMatchingFilterChainResolver();
//2、创建FilterChainManager
DefaultFilterChainManager filterChainManager = new DefaultFilterChainManager();
//3、注册基本的Filter
for(DefaultFilter filter : DefaultFilter.values()) {
filterChainManager.addFilter(
filter.name(), (Filter) ClassUtils.newInstance(filter.getFilterClass()));
}
//4、注册URL-Filter的映射关系
filterChainManager.addToChain("/login.jsp", "authc");
filterChainManager.addToChain("/unauthorized.jsp", "anon");
filterChainManager.addToChain("/**", "authc");
filterChainManager.addToChain("/**", "roles", "admin");
//5、设置Filter的属性
FormAuthenticationFilter authcFilter =
(FormAuthenticationFilter)filterChainManager.getFilter("authc");
authcFilter.setLoginUrl("/login.jsp");
RolesAuthorizationFilter rolesFilter =
(RolesAuthorizationFilter)filterChainManager.getFilter("roles");
rolesFilter.setUnauthorizedUrl("/unauthorized.jsp");
filterChainResolver.setFilterChainManager(filterChainManager);
return filterChainResolver;
}
}
在web.xml中配置该Environment:
<context-param> <param-name>shiroEnvironmentClass</param-name> <param-value> com.github.zhangkaitao.shiro.chapter8.web.env.MyIniWebEnvironment </param-value> </context-param>
代码实例:ideaProjects/shiroChapter8
- 参考文章
web项目的拦截器链配置
若果定义多个同url的拦截器链,那么Shiro会将其合并,也就是说都有效;但如果当前请求匹配多条拦截器链的话,就只有第一个有效。
<!-- Shiro的Web过滤器 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="filters"> <util:map> <entry key="statelessAuthc" value-ref="statelessAuthcFilter"/> </util:map> </property> <property name="filterChainDefinitions"> <value> /login.jsp = authc /logout = logout /** = statelessAuthc </value> </property> </bean>