简介

  • 即访问url时进行权限匹配,如果没有权限则直接跳到错误页面。
  • 在Shiro中,更多的是通过AOP进行方法级别的权限控制,而通过url进行权限控制是一种集中的权限控制。

示例

  • 拦截器:如authc、anon等字符串为一个拦截器;拦截器链:url=authc为一条拦截器链;拦截器链集合:url1=authc,url2=anon为拦截器链集合。
  • 仿chapter16的示例,主要功能是用户、公司、角色和资源之间关系的调整,如修改用户所属公司、所有角色,资源所需权限、角色所有资源等。
  • 数据库:除user、organization、role、resource外,新增一张url-filter表,映射url和角色、权限之间的关系(多个角色、权限之间用逗号分隔),即动态的拦截器链。
  • 实体:多一个URLFilter类,封装url和权限、角色间的关系。
public class UrlFilter implements Serializable {
    private Long id;
    private String name; //url名称或描述
    private String url; //地址
    private String roles; //所需角色,可省略,用逗号分隔
    private String permissions; //所需权限,可省略,用逗号分隔

    //setter/getter
}
  • dao、service、controller层都增加对UrlFilter的增删查改。
  • controller层撤销shiro的权限注解,因为要用数据库中动态的拦截器链来实现。
  • 因为数据库中的拦截器链都是要合并到shiro中的,所以在service层中,更新数据库拦截器链后,要重置shiro中的拦截器链集合,以同步此更新,否则更新不生效。比如:
@Override
public void deleteUrlFilter(Long urlFilterId) {
    urlFilterDao.deleteUrlFilter(urlFilterId);
    initFilterChain();
}

/**
 * @Author haien
 * @Description 初始化Shiro的url拦截器,用于在url表改动后,
                将数据库发送的改动同步到Shiro中。
 * @Date 2019/4/5
 * @Param []
 * @return void
 **/
@PostConstruct //当DI容器实例化当前bean时,该方法会自动执行
public void initFilterChain() {
    //将配置文件和数据库的拦截器链合并
    shiroFilerChainManager.initFilterChains(findAll()); //先清空集合再去加载
}
  • ShiroFilterChainManager: 调用了FilterChainManager的方法,先清空拦截器链集合再重新加载。
@Service
public class ShiroFilterChainManager {
    //过滤链管理器,管理所有过滤器,包含增删查改等操作
    @Resource
    private DefaultFilterChainManager filterChainManager; //注入的是
                                                    //CustomDefaultFilterChainManager
    //默认过滤链,包含在配置文件中提到的所有过滤器
    private Map<String,NamedFilterList> defaultFilterChains;

    /**
     * @Author haien
     * @Description 获取配置文件中all过滤器,之后会自动与数据库中包含的过滤器合并
     * @Date 2019/4/5
     * @Param []
     * @return void
     **/
    @PostConstruct //DI容器实例化该bean时自动执行该方法
    public void inti(){
        //获取配置文件中all拦截器(静态的,不会变)
        defaultFilterChains=new HashMap<>(filterChainManager.getFilterChains());
    }

    /**
     * @Author haien
     * @Description 初始化过滤链,默认+数据库
     * @Date 2019/4/5
     * @Param [urlFilters]
     * @return void
     **/
    public void initFilterChains(List<UrlFilter> urlFilters){
        //1. 删除以前老的filter chain并注册默认的
        filterChainManager.getFilterChains().clear();
        if(defaultFilterChains!=null)
            filterChainManager.getFilterChains().putAll(defaultFilterChains);

        //2. 注册数据库中的
        for(UrlFilter urlFilter:urlFilters){
            String url=urlFilter.getUrl();
            //注册roles-filter关系的过滤器
            if(!StringUtils.isEmpty(urlFilter.getRoles()))
                filterChainManager.addToChain(url,"roles",urlFilter.getRoles());

            //注册perms-filter关系的拦截器
            if(!StringUtils.isEmpty(urlFilter.getPermissions()))
                filterChainManager.addToChain(url,"perms",urlFilter.getPermissions());
        }
    }
}
  • 为了实现本例功能,加载数据库中动态的拦截器链,我们需要自定义FilterChainManager;而为了实现一个url匹配多个拦截器链,我们还要自定义FilterChainResolver。下面先介绍一下这俩都是什么。

PathMatchingFilterChainResolver过滤链解析器

  • 按照第八章过滤链步骤,第一步是要先根据当前请求的url获取拦截器链。
  • PathMatchingFilterChainResolver:遍历配置文件中all拦截器链,如,/=athc,将其名字/与当前url/index比对,匹配则执行该拦截器链,如,/**匹配/index。所以,如果要改变url的拦截器匹配方法,则要自定义Resolver。
public FilterChain getChain(ServletRequest request, ServletResponse response, 
        FilterChain originalChain) {
    //1、首先获取拦截器链管理器
    FilterChainManager filterChainManager = getFilterChainManager();
    if (!filterChainManager.hasChains()) {
        return null;
    }

    //2、接着获取当前请求的URL(不带上下文)
    String requestURI = getPathWithinApplication(request);

    //3、遍历配置文件中all拦截器链(拦截器链的名字就是URL,如/**)
    for (String pathPattern : filterChainManager.getChainNames()) {
        //4、如匹配当前url
        if (pathMatches(pathPattern, requestURI)) {
            //5、和原始拦截器链进行合并后返回
            return filterChainManager.proxy(originalChain, pathPattern);
        }
    }
    return null;
}
  • 缺点:最后一步找到即返回,如果后面还有其他拦截器也是匹配当url的呢?后面我们会自定义一个能将all拦截器链合并返回的解析器。

  • 第1步中,默认使用DefaultFilterChainManager管理拦截器链,其内部采用Map管理url-拦截器链的关系,因为Map不可重复,所以每个url只能配置一条拦截器链(但可以包含多个拦截器),如果定义了多条则最终采用哪条也说不定。

  • FilterChainManager接口:主要功能是注册拦截器链、添加拦截器以及对原始拦截器链生成代理之后的拦截器链。如果要实现动态url-拦截器配置,即注册数据库中定义的拦截器链的话,就要自定义Manager。
public interface FilterChainManager {  
    Map<String, Filter> getFilters(); //得到注册的拦截器  

    void addFilter(String name, Filter filter); //注册拦截器  

    void addFilter(String name, Filter filter, boolean init); //注册拦截器  

    //根据拦截器链定义创建拦截器链 
    void createChain(String chainName, String chainDefinition);  

    void addToChain(String chainName, String filterName); //添加拦截器到指定的拦截器链  

    //添加拦截器(带配置,指明所需权限或角色)到指定的拦截器链  
    void addToChain(String chainName, String filterName, 
        String chainSpecificFilterConfig) throws ConfigurationException; 

    NamedFilterList getChain(String chainName); //获取拦截器链  

    boolean hasChains(); //是否有拦截器链  

    Set<String> getChainNames(); //得到所有拦截器链的名字  

    //使用指定的拦截器链代理原始拦截器链 
    FilterChain proxy(FilterChain original, String chainName); 
}
  • 解析以下配置文件:filters定义了拦截器,filterChainDefinitions定义了拦截器链,/login为拦截器链名字,user,sysUser是拦截器名字。

    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">  
        ……  
        <property name="filters">  
            <util:map>  
                <entry key="authc" value-ref="formAuthenticationFilter"/>  
                <entry key="sysUser" value-ref="sysUserFilter"/>  
            </util:map>  
        </property>  
        <property name="filterChainDefinitions">  
            <value>  
                /login = authc  
                /logout = logout  
                /authenticated = authc  
                /** = user,sysUser  
            </value>  
        </property>  
    </bean>   
    
  • 前面说过PathMatchingFilterChainResolver和DefaultFilterChainManager不能满足我们的需求,我们稍微扩展一下。

  • CustomPathMatchingFilterChainResolver:本例自定义的Resolver;返回匹配当前url的多条拦截器链,默认的只能返回第一条。
public class CustomPathMatchingFilterChainResolver  
    extends PathMatchingFilterChainResolver {  //继承自默认解析器

      private CustomDefaultFilterChainManager customDefaultFilterChainManager;  

      //Manager setter
      public void setCustomDefaultFilterChainManager(  
            CustomDefaultFilterChainManager customDefaultFilterChainManager) {  

          this.customDefaultFilterChainManager = customDefaultFilterChainManager;  
          setFilterChainManager(customDefaultFilterChainManager);  
      }  

      //为当前url匹配拦截器链,匹配多条,而不是遇到一条就返回
      @Override
      public FilterChain getChain(ServletRequest request, ServletResponse response, 
            FilterChain originalChain) {  

          FilterChainManager filterChainManager = getFilterChainManager();  
          if (!filterChainManager.hasChains()) {  
              return null;  
          }  

          //遍历配置文件all拦截器链
          String requestURI = getPathWithinApplication(request);  
          List<String> chainNames = new ArrayList<String>(); //拦截器链集合
          for (String pathPattern : filterChainManager.getChainNames()) {  
            if (pathMatches(pathPattern, requestURI)) {  
                //遇到匹配的不急着返回,而是添加到集合
                chainNames.add(pathPattern);   
            }  
          }  

          //集合为空则返回null
          if(chainNames.size() == 0) {  
            return null;  
          }  

          return customDefaultFilterChainManager.proxy(originalChain, chainNames);  
      }  
    }
  • CustomDefaultFilterChainManager:本例自定义的Manager;主要是改变注册拦截器链的方式,比如说吧数据库中的合并进来。
public class CustomDefaultFilterChainManager extends DefaultFilterChainManager {  
    //配置文件中all拦截器链,即默认的静态拦截器链,会与数据库中动态的拦截器链合并
    private Map<String, String> filterChainDefinitionMap = null;  
    //登录地址
    private String loginUrl;  
    //登录成功后默认跳转地址
    private String successUrl;
    //未授权跳转地址
    private String unauthorizedUrl;  

    public CustomDefaultFilterChainManager() {  
        //调用父类方法,注册默认拦截器
        setFilters(new LinkedHashMap<String, Filter>());  
        //注册默认拦截器链
        setFilterChains(new LinkedHashMap<String, NamedFilterList>());  
        addDefaultFilters(true);  
    }  

    //省略setter

    //注册我们自定义的拦截器,相当于ShiroFilterFactoryBean的filters属性
    //只要是我们在配置文件中声明注册bean的,不管shiro本身提供的还是我们自定义的拦截器,
    //都算自定义拦截器
    public void setCustomFilters(Map<String, Filter> customFilters) {  
        for(Map.Entry<String, Filter> entry : customFilters.entrySet()) {  
            addFilter(entry.getKey(), entry.getValue(), false);  
        }  
    }  

    //解析配置文件中all拦截器链,相当于ShiroFilterFactoryBean的filterChainDefinitions
    public void setDefaultFilterChainDefinitions(String definitions) {  
        Ini ini = new Ini();  
        ini.load(definitions);  
        Ini.Section section = ini.getSection(IniFilterChainResolverFactory.URLS);  
        if (CollectionUtils.isEmpty(section)) {  
            section = ini.getSection(Ini.DEFAULT_SECTION_NAME);  
        }  
        setFilterChainDefinitionMap(section);  
    }  

    @PostConstruct //Spring容器启动时自动调用
    public void init() {  
        ////给拦截器配置上属性,这里是三个url
        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); //给拦截器设置三个url 
                if (filter instanceof Nameable) {  
                    ((Nameable) filter).setName(name);  
                }
                addFilter(name, filter, false);  
            }  
        }  

        //将拦截器构建成拦截器链
        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(); //如,authc,user
                createChain(url, chainDefinition); //创建过滤器链 
            }  
        }  
    }  

    protected void initFilter(Filter filter) {  
        //ignore,因为交给Spring管理了,因此Filter的相关配置会在Spring配置中完成  
    }  

    //组合多个拦截器链为一个新的FilterChain代理
    public FilterChain proxy(FilterChain original, List<String> chainNames) {
        //chainNames中的元素,如,/index、/**
        NamedFilterList configured = new SimpleNamedFilterList(chainNames.toString()); 

        //遍历过滤器链名,找到对应的过滤器链,添加到集合
        for(String chainName : chainNames) {  
            configured.addAll(getChain(chainName));  
        }

        //再和原始的过滤器链合并
        return configured.proxy(original);  
    }  

    private void applyGlobalPropertiesIfNecessary(Filter filter) {  
        applyLoginUrlIfNecessary(filter);  
        applySuccessUrlIfNecessary(filter);  
        applyUnauthorizedUrlIfNecessary(filter);  
    }  
    private void applyLoginUrlIfNecessary(Filter filter) {  
        ...配置loginUrl...
    }  
    private void applySuccessUrlIfNecessary(Filter filter) {  
        ...配置SuccessUrl...
    }  
    private void applyUnauthorizedUrlIfNecessary(Filter filter) {  
        ...配置UnauthorizedUrl...
    }  
}
  • spring-config-shiro.xml: 其中拦截器链的配置较特别。

    <!-- 基于Form表单的身份验证过滤器 -->
    <bean id="formAuthenticationFilter"
          class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter">
        <property name="usernameParam" value="username"/>
        <property name="passwordParam" value="password"/>
        <property name="rememberMeParam" value="rememberMe"/>
        <property name="loginUrl" value="/login"/>
    </bean>
    <bean id="sysUserFilter"
          class="com.haien.chapter16.web.shiro.filter.SysUserFilter"/>
    <!--自定义的拦截器链管理器,要添加数据库中动态的拦截器链就必须自定义-->
    <bean id="filterChainManager"
          class="com.haien.chapter19.service.impl.CustomDefaultFilterChainManager">
        <property name="loginUrl" value="/login"/>
        <property name="successUrl" value="/"/>
        <property name="unauthorizedUrl" value="/unauthorized.jsp"/>
        <property name="customFilters">
            <util:map>
                <entry key="authc" value-ref="formAuthenticationFilter"/>
                <entry key="sysUser" value-ref="sysUserFilter"/>
            </util:map>
        </property>
        <property name="filterChainDefinitions">
            <value>
                /login = authc
                /logout = logout
                /authenticated = authc
                /** = user,sysUser
            </value>
        </property>
    </bean>
    <!--拦截器链解析器,为当前url匹配多条拦截器链;注入以上管理器-->
    <bean id="filterChainResolver"
          class="com.haien.chapter19.service.impl.CustomPathMatchingFilterChainResolver">
        <property name="customDefaultFilterChainManager" ref="filterChainManager"/>
    </bean>
    <!-- Shiro的Web过滤器 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <!--拦截器链配置已被拦截器链管理器代劳
        <property name="loginUrl" value="/login"/>
        <property name="filters">
            <util:map>
                <entry key="authc" value-ref="formAuthenticationFilter"/>
                <entry key="sysUser" value-ref="sysUserFilter"/>
            </util:map>
        </property>
        <property name="filterChainDefinitions">
            <value>
                /login = authc
                /logout = logout
                /authenticated = authc
                /** = user,sysUser
            </value>
        </property>
        -->
    </bean>
    <!--最后把filterChainResolver设置给ShiroFilter,其使用它进行动态url动态控制-->
    <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
        <property name="targetObject" ref="shiroFilter"/>
        <property name="targetMethod" ref="setFilterChainResolver"/>
        <property name="arguments" ref="filterChainResolver"/>
    </bean>
    
  • 测试:

  1. 访问/urlFilter;
  2. 跳转登录页面,登录后点击新增,增加以下拦截器链:

不用分配任何权限。

  1. 再访问/user则要求用户拥有aa角色,因此跳转未授权页面。
  2. 在数据库中将aa角色添加给用户,再访问/user则可以访问了。