拦截器

  • 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间接子类。

拦截器调用链

  • 从高到低:
  1. AbstractShiroFilter.executeChain(): 开始过滤链。
  2. PathMatchingChainResolver.getExecutionChain(): 获取过滤链,如,/** = kickout,user,sysUser配置了三个过滤链;doFilter(): 执行第一个拦截器。
  3. doFilterInternal():由doFilter()调用;调用preHandle(),返回true则调用executeChain()继续执行下一拦截器。
  4. AdviceFilter.executeChain():执行下一拦截器。
  5. doFilter():开始执行。

FormAuthenticationFilter调用链

  • 扩展上面第3步,调用链从高级到低级:
  1. PathMatchingFilter.preHandle():访问某url,ShiroFilter拦截该url,调用该方法判断它指定了哪个Filter,然后这个Filter实现了哪些方法就执行哪些方法。
  2. AccessControlFilter.onPreHandle():如果实现了该方法的话,它将会调用isAccessAllow()和onAccessDenied(),但通常是直接让前者返回false,主要的验证逻辑放在后者实现;或者主要研验证逻辑放在前者实现,后者做响应的校验失败处理,比如把失败信息存入request。
  3. FormAuthenticationFilter.onAccessDenied():在需要权限的方法执行前调用,而不是被拒绝访问后才执行,不要望文生义。比如判断用户是否已登录,未登录则重定向至登录页面,正在登录则调用以下login()接收表单验证,已登录则继续过滤链,最终访问到目标方法。
  4. DelegatingSubject.login()
  5. ModularRealmAuthenticator.doAuthenticate()
  6. 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>