ShiroFilterFactoryBean源码及拦截原理深入分析

Stella981
• 阅读 329

本篇文章篇幅比较长,但是细看下去相信对学习Shiro应该会有帮助。好了,闲话不多说,直接进入正题:

Shiro提供了与Web集成的支持,其通过一个ShiroFilter入口来拦截需要安全控制的URL,然后进行相应的控制,ShiroFilter类似于如Strut2/SpringMVC这种web框架的前端控制器,其是安全控制的入口点,其负责读取配置(如ini配置文件),然后判断URL是否需要登录/权限等工作。

而要在Spring中使用Shiro的话,可在web.xml中配置一个DelegatingFilterProxyDelegatingFilterProxy作用是自动到Spring容器查找名字为shiroFilterfilter-name)的bean并把所有Filter的操作委托给它。

首先是在web.xml中配置DelegatingFilterProxy

<filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <init-param>
        <param-name>targetFilterLifecycle</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

配置好DelegatingFilterProxy后,下面只要再把ShiroFilter配置到Spring容器(此处为Spring的配置文件)即可:

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="securityManager" ref="securityManager"/>
</bean>

可以看到我们使用了ShiroFilterFactoryBean来创建shiroFilter,这里用到了Spring中一种特殊的Bean——FactoryBean。当需要得到名为”shiroFilter“的bean时,会调用其getObject()来获取实例。下面我们通过分析ShiroFilterFactoryBean创建实例的过程来探究Shiro是如何实现安全拦截的:

 public Object getObject() throws Exception {
        if (instance == null) {
            instance = createInstance();
        }
        return instance;
    }

其中调用了createInstance()来创建实例:

 protected AbstractShiroFilter createInstance() throws Exception {

        // 这里是通过FactoryBean注入的SecurityManager(必须)
        SecurityManager securityManager = getSecurityManager();
        if (securityManager == null) {
            String msg = "SecurityManager property must be set.";
            throw new BeanInitializationException(msg);
        }

        if (!(securityManager instanceof WebSecurityManager)) {
            String msg = "The security manager does not implement the WebSecurityManager interface.";
            throw new BeanInitializationException(msg);
        }

        FilterChainManager manager = createFilterChainManager();

        PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
        chainResolver.setFilterChainManager(manager);

        return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
    }

可以看到创建SpringShiroFilter时用到了两个组件:SecurityManagerChainResolver

  • SecurityManager:我们知道其在Shiro中的地位,类似于一个“安全大管家”,相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher,是Shiro的心脏,所有具体的交互都通过SecurityManager进行控制,它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。
  • ChainResolver:Filter链解析器,用来解析出该次请求需要执行的Filter链。
  • PathMatchingFilterChainResolverChainResolver的实现类,其中还包含了两个重要组件FilterChainManagerPatternMatcher
  • FilterChainManager:管理着Filter和Filter链,配合PathMatchingFilterChainResolver解析出Filter链
  • PatternMatcher:用来进行请求路径匹配,默认为Ant风格的路径匹配

先有一个大体的了解,那么对于源码分析会有不少帮助。下面会对以上两个重要的组件进行分析,包括PathMatchingFilterChainResolverFilterChainManager。首先贴一段ShiroFilter的在配置文件中的定义:

<!-- Shiro的Web过滤器 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager" />
        <property name="loginUrl" value="/login" />
        <property name="unauthorizedUrl" value="/special/unauthorized" />
        <property name="filters">
            <util:map>
                <entry key="authc" value-ref="formAuthenticationFilter" />
                <entry key="logout" value-ref="logoutFilter" />
                <entry key="ssl" value-ref="sslFilter"></entry>
            </util:map>
        </property>
        <property name="filterChainDefinitions">
            <value>
                /resources/** = anon
                /plugin/** = anon
                /download/** = anon
                /special/unauthorized = anon
                /register = anon
                /login = ssl,authc
                /logout = logout
                /admin/** = roles[admin]

                /** = user
            </value>
        </property>
    </bean>

再来看看PathMatchingFilterChainResolverFilterChainManager的创建过程:

protected FilterChainManager createFilterChainManager() {

        // 默认使用的FilterChainManager是DefaultFilterChainManager
        DefaultFilterChainManager manager = new DefaultFilterChainManager();
        // DefaultFilterChainManager默认会注册的filters(后面会列出)
        Map<String, Filter> defaultFilters = manager.getFilters();

        // 将ShiroFilterFactoryBean配置的一些公共属性(上面配置的loginUrl,successUrl,unauthorizeUrl)应用到默认注册的filter上去
        for (Filter filter : defaultFilters.values()) {
            applyGlobalPropertiesIfNecessary(filter);
        }

        // 处理自定义的filter(上面配置的filters属性),步骤类似上面
        Map<String, Filter> filters = getFilters();
        if (!CollectionUtils.isEmpty(filters)) {
            for (Map.Entry<String, Filter> entry : filters.entrySet()) {
                String name = entry.getKey();
                Filter filter = entry.getValue();
                applyGlobalPropertiesIfNecessary(filter);
                if (filter instanceof Nameable) {
                    ((Nameable) filter).setName(name);
                }
                // 将Filter添加到manager中去,可以看到对于Filter的管理是依赖于FilterChainManager的
                manager.addFilter(name, filter, false);
            }
        }

        // 根据FilterChainDefinition的配置来构建Filter链(上面配置的filterChainDefinitions属性)
        Map<String, String> chains = getFilterChainDefinitionMap();
        if (!CollectionUtils.isEmpty(chains)) {
            for (Map.Entry<String, String> entry : chains.entrySet()) {
                String url = entry.getKey();
                String chainDefinition = entry.getValue();
                // 后面会分析该步的源码,功能上就是创建Filter链
                manager.createChain(url, chainDefinition);
            }
        }

        return manager;
    }

下面有必要来看看DefaultFilterChainManager的源码,分析一下上面调用到的方法。先来看看他的几个重要的属性:

  private FilterConfig filterConfig;

    private Map<String, Filter> filters; //pool of filters available for creating chains

    private Map<String, NamedFilterList> filterChains; //key: chain name, value: chain

其中filterConfig仅在初始化Filter时有效,而我们自定义的Filter都不是init的,所以该属性可以暂时忽略()。 
而后面两张map就重要了:filters中缓存了所有添加的filter,filterChains则缓存了所有的filterChain。其中前者的key是filter name,value是Filter。而后者的key是chain name,value是NamedFilterList。 
有的童鞋可能会问NamedFilterList是怎么样的结构呢,你可以把它当成List<Filter>,这样就好理解了吧。下面再分析刚才createFilterChainManager()中调用过的manager的几个方法:

  • addFilter(缓存filter让manager来管理)

  •   public void addFilter(String name, Filter filter, boolean init) {
              addFilter(name, filter, init, true);
          }
        
          protected void addFilter(String name, Filter filter, boolean init, boolean overwrite) {
              Filter existing = getFilter(name);
              if (existing == null || overwrite) {
                  if (filter instanceof Nameable) {
                      ((Nameable) filter).setName(name);
                  }
                  if (init) {
                      initFilter(filter);
                  }
                  this.filters.put(name, filter);
              }
          }
    

    filter缓存到filters这张map里,不管是默认注册的还是自定义的都需要FilterChainManager来统一管理。

  • createChain:创建filterChain并将定义的filter都加进去

  •   // chainName就是拦截路径"/resources/**",chainDefinition就是多个过滤器名的字符串
          public void createChain(String chainName, String chainDefinition) {
              if (!StringUtils.hasText(chainName)) {
                  throw new NullPointerException("chainName cannot be null or empty.");
              }
              if (!StringUtils.hasText(chainDefinition)) {
                  throw new NullPointerException("chainDefinition cannot be null or empty.");
              }
        
              if (log.isDebugEnabled()) {
                  log.debug("Creating chain [" + chainName + "] from String definition [" + chainDefinition + "]");
              }
      
              // 先分离出配置的各个filter,比如 
              // "authc, roles[admin,user], perms[file:edit]" 分离后的结果是:
              // { "authc", "roles[admin,user]", "perms[file:edit]" }
              String[] filterTokens = splitChainDefinition(chainDefinition);
      
              // 进一步分离出"[]"内的内容,其中nameConfigPair是一个长度为2的数组
              // 比如 roles[admin,user] 经过解析后的nameConfigPair 为{"roles", "admin,user"}
              for (String token : filterTokens) {
                  String[] nameConfigPair = toNameConfigPair(token);
      
                  // 得到了 拦截路径、filter以及可能的"[]"中的值,那么执行addToChain
                  addToChain(chainName, nameConfigPair[0], nameConfigPair[1]);
              }
          }
    

    至此,FilterChainManager就创建完了,它无非就是缓存了两张map,没有什么逻辑上的操作。下面将FilterChainManager设置到PathMatchingFilterChainResolver中。PathMatchingFilterChainResolver实现了FilterChainResolver接口,该接口中只定义了一个方法:

  •   FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain);
    

    通过解析请求来得到一个新的FilterChain。而PathMatchingFilterChainResolver实现了该接口,依靠了FilterChainManager中保存的chainFiltersfilters这两张map来根据请求路径解析出相应的filterChain,并且和originalChain组合起来使用。下面具体看看PathMatchingFilterChainResolver中的实现:

       public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
              // 得到 FilterChainManager 
              FilterChainManager filterChainManager = getFilterChainManager();
              if (!filterChainManager.hasChains()) {
                  return null;
              }
      
              String requestURI = getPathWithinApplication(request);
      
              // chainNames就是刚定义的filterChains的keySet,也就是所有的路径集合(比如:["/resources/**","/login"])
              for (String pathPattern : filterChainManager.getChainNames()) {
      
                  // 请求路径是否匹配某个 定义好的路径:
                  if (pathMatches(pathPattern, requestURI)) {
                      if (log.isTraceEnabled()) {
                          log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "].  " + "Utilizing corresponding filter chain...");
                      }
                      // 找到第一个匹配的Filter链,那么就返回一个ProxiedFilterChain
                      return filterChainManager.proxy(originalChain, pathPattern);
                  }
              }
      
              return null;
          }
    

    这里返回只有两种情况,要么是null,要么就是一个ProxiedFilterChain。返回null并不表示中断FilterChain,而是只用originChain。而关于ProxiedFilterChain,它实现了FilterChain,内部维护了两份FilterChain(其实一个是FilterChain,另一个是List<Filter>) 
    FilterChain也就是web.xml中注册的Filter形成的FilterChain,我们称之为originChain。而另一个List<Filter>则是我们在Shiro中注册的Filter链了,下面看看ProxiedFilterChain中关于doFilter(...)的实现:

       public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
              if (this.filters == null || this.filters.size() == this.index) {
                  //we've reached the end of the wrapped chain, so invoke the original one:
                  if (log.isTraceEnabled()) {
                      log.trace("Invoking original filter chain.");
                  }
                  this.orig.doFilter(request, response);
              } else {
                  if (log.isTraceEnabled()) {
                      log.trace("Invoking wrapped filter at index [" + this.index + "]");
                  }
                  this.filters.get(this.index++).doFilter(request, response, this);
              }
          }
    

    可以看到,它会先执行Shiro中执行的filter,然后再执行web.xml中的Filter。不过要注意的是,需要等到originChain执行到ShiroFilter之后才会执行Shiro中的Filter链。 
    至此,两个组件的创建过程差不多都介绍完了,那么当这两个组件创建完毕后,是如何工作的呢? 
    先从ShiroFilter入手,因为它是总的拦截器,看看其中的doFilterInternal(...)方法:

      protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
                  throws ServletException, IOException {
      
              Throwable t = null;
      
              try {
                  final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
                  final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
      
                  final Subject subject = createSubject(request, response);
      
                  //noinspection unchecked
                  subject.execute(new Callable() {
                      public Object call() throws Exception {
                          // 其实需要关心的就在这里
                          // touch一下session
                          updateSessionLastAccessTime(request, response);
                          // 执行Filter链
                          executeChain(request, response, chain);
                          return null;
                      }
                  });
              } catch (ExecutionException ex) {
                  t = ex.getCause();
              } catch (Throwable throwable) {
                  t = throwable;
              }
      
              if (t != null) {
                  if (t instanceof ServletException) {
                      throw (ServletException) t;
                  }
                  if (t instanceof IOException) {
                      throw (IOException) t;
                  }
                  //otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one:
                  String msg = "Filtered request failed.";
                  throw new ServletException(msg, t);
              }
          }
    

    跟进executeChain(...)方法:

      protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain)
                  throws IOException, ServletException {
              FilterChain chain = getExecutionChain(request, response, origChain);
              chain.doFilter(request, response);
          }
    

    如何得到FilterChain的呢?如果你认真的看到这里,那么你应该不难想到其中肯定利用了刚才注册的ChainResolver

       protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
              FilterChain chain = origChain;
      
              FilterChainResolver resolver = getFilterChainResolver();
              if (resolver == null) {
                  log.debug("No FilterChainResolver configured.  Returning original FilterChain.");
                  return origChain;
              }
      
              FilterChain resolved = resolver.getChain(request, response, origChain);
              if (resolved != null) {
                  log.trace("Resolved a configured FilterChain for the current request.");
                  chain = resolved;
              } else {
                  log.trace("No FilterChain configured for the current request.  Using the default.");
              }
      
              return chain;
          }
    

    猜对了~并且也验证了当resolver.getChain(...)返回null时,直接使用originChain了。然后执行返回的FilterChaindoFilter(...)方法。这个过程我们再脱离代码来分析一下:当我们从浏览器发出一个请求,究竟发生了什么? 
    这里只站在Filter的层面来分析。服务器启动后,读取web.xml中的filterfilter-mapping节点后组成FilterChain,对请求进行拦截。拦截的顺序按照filter节点的定义顺序,Shiro利用ShiroFilter来充当一个总的拦截器来分发所有需要被Shiro拦截的请求,所以我们看到在Shiro中我们还可以自定义拦截器。ShiroFilter根据它在拦截器中的位置,只要执行到了那么就会暂时中断原FilterChain的执行,先执行Shiro中定义的Filter,最后再执行原FilterChian。可以打个比方,比如说本来有一条铁链,一直蚂蚁从铁链的开端往末端爬,其中某一环叫ShiroFilter,那么当蚂蚁爬到ShiroFilter这一环时,将铁链打断,并且接上另一端铁链(Shiro中自定义的Filter),这样就构成了一条新的铁链。然后蚂蚁继续爬行(后续的执行过程)。

    到这里,我们已经根据请求路径找到了一条Filter链(originChain + shiroChain),之后就是对链上的FilterdoFilter,其中关于如何 
    就是filter后配置的[]部分是如何生效的,我们可以看PathMatchingFilter中的Prehandle(...)方法:

      protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
      
              if (this.appliedPaths == null || this.appliedPaths.isEmpty()) {
                  if (log.isTraceEnabled()) {
                      log.trace("appliedPaths property is null or empty.  This Filter will passthrough immediately.");
                  }
                  return true;
              }
      
              // appliedPaths中保存了该filter中能拦截的路径和该路径配置的key-value对,比如{key="/admin/**", value="[admin]"}
              for (String path : this.appliedPaths.keySet()) {
                  // 首先是匹配路径
                  if (pathsMatch(path, request)) {
                      log.trace("Current requestURI matches pattern '{}'.  Determining filter chain execution...", path);
                      // 然后开始验证“[]”中的字符串
                      Object config = this.appliedPaths.get(path);
                      return isFilterChainContinued(request, response, path, config);
                  }
              }
      
              //no path matched, allow the request to go through:
              return true;
          }
    

    下面跟踪isFilterChainContinued(...)

      private boolean isFilterChainContinued(ServletRequest request, ServletResponse response,
                                                 String path, Object pathConfig) throws Exception {
      
              if (isEnabled(request, response, path, pathConfig)) { //isEnabled check added in 1.2
                  if (log.isTraceEnabled()) {
                       // log
                  }
                  return onPreHandle(request, response, pathConfig);
              }
      
              if (log.isTraceEnabled()) {
                  // log
              }
              return true;
          }
    

    基本也就是交给onPreHandle(...)来处理,所以一般需要验证”[]“中字符串的filter都会扩展这个方法,比如AccessControlFilter

      public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
              return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
          }
    

    RolesAuthorizationFilter中:

       public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
      
              Subject subject = getSubject(request, response);
              String[] rolesArray = (String[]) mappedValue;
      
              if (rolesArray == null || rolesArray.length == 0) {
                  //no roles specified, so nothing to check - allow access.
                  return true;
              }
      
              Set<String> roles = CollectionUtils.asSet(rolesArray);
              return subject.hasAllRoles(roles);
          }
    

    最后附上默认注册的filters

      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);
      }
    
点赞
收藏
评论区
推荐文章
光头强的博客 光头强的博客
2个月前
Java面向对象试题
1、 请创建一个Animal动物类,要求有方法eat()方法,方法输出一条语句“吃东西”。 创建一个接口A,接口里有一个抽象方法fly()。创建一个Bird类继承Animal类并实现 接口A里的方法输出一条有语句“鸟儿飞翔”,重写eat()方法输出一条语句“鸟儿 吃虫”。在Test类中向上转型创建b对象,调用eat方法。然后向下转型调用eat()方
刚刚好 刚刚好
2个月前
css问题
1、 在IOS中图片不显示(给图片加了圆角或者img没有父级) <div<img src""/</div div {width: 20px; height: 20px; borderradius: 20px; overflow: h
blmius blmius
1年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录 问题 用navicat导入数据时,报错: 原因这是因为当前的MySQL不支持datetime为0的情况。 解决修改sql\mode: sql\mode:SQL Mode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。 全局s
晴空闲云 晴空闲云
2个月前
css中box-sizing解放盒子实际宽高计算
我们知道传统的盒子模型,如果增加内边距padding和边框border,那么会撑大整个盒子,造成盒子的宽度不好计算,在实务中特别不方便。boxsizing可以设置盒模型的方式,可以很好的设置固定宽高的盒模型。 盒子宽高计算假如我们设置如下盒子:宽度和高度均为200px,那么这会这个盒子实际的宽高就都是200px。但是当我们设置这个盒子的边框和内间距的时候,那
艾木酱 艾木酱
1个月前
快速入门|使用MemFire Cloud构建React Native应用程序
> MemFire Cloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专
Easter79 Easter79
1年前
Twitter的分布式自增ID算法snowflake (Java版)
概述 == 分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。 有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。 而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
1年前
MySQL查询按照指定规则排序
1.按照指定(单个)字段排序 select * from table_name order id desc; 2.按照指定(多个)字段排序 select * from table_name order id desc,status desc; 3.按照指定字段和规则排序 selec
Stella981 Stella981
1年前
Angular material mat
Icon Icon Name mat-icon code _add\_comment_ add comment icon <mat-icon> add\_comment</mat-icon> _attach\_file_ attach file icon <mat-icon> attach\_file</mat-icon> _attach\
Wesley13 Wesley13
1年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
#### 背景描述 # Time: 2019-01-24T00:08:14.705724+08:00 # User@Host: **[**] @ [**] Id: ** # Schema: sentrymeta Last_errno: 0 Killed: 0 # Query_time: 0.315758 Lock_
helloworld_34035044 helloworld_34035044
4个月前
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。 uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid() 或 uuid(sep)参数说明:sep 布尔值,生成的uuid中是否包含分隔符'',缺省为
helloworld_28799839 helloworld_28799839
2个月前
常用知识整理
# Javascript ## 判断对象是否为空 ```js Object.keys(myObject).length === 0 ``` ## 经常使用的三元运算 > 我们经常遇到处理表格列状态字段如 `status` 的时候可以用到 ``` vue