动态 URL 权限控制
用过 Spring Security 的朋友应该比较熟悉对 URL 进行全局的权限控制,即访问 URL 时进行权限匹配;如果没有权限直接跳到相应的错误页面。Shiro 也支持类似的机制,不过需要稍微改造下来满足实际需求。不过在 Shiro 中,更多的是通过 AOP 进行分散的权限控制,即方法级别的;而通过 URL 进行权限控制是一种集中的权限控制。本章将介绍如何在 Shiro 中完成动态 URL 权限控制。
本章代码基于《第十六章 综合实例》,请先了解相关数据模型及基本流程后再学习本章。
表及数据 SQL
请运行 shiro-example-chapter19/sql/ shiro-schema.sql 表结构
请运行 shiro-example-chapter19/sql/ shiro-schema.sql 数据
实体
具体请参考 com.github.zhangkaitao.shiro.chapter19 包下的实体。
1 | public class UrlFilter implements Serializable { |
表示拦截的 URL 和角色 / 权限之间的关系,多个角色 / 权限之间通过逗号分隔,此处还可以扩展其他的关系,另外可以加如 available 属性表示是否开启该拦截。
DAO
具体请参考 com.github.zhangkaitao.shiro.chapter19.dao 包下的 DAO 接口及实现。
Service
具体请参考 com.github.zhangkaitao.shiro.chapter19.service 包下的 Service 接口及实现。
1 | public interface UrlFilterService { |
基本的 URL 拦截的增删改查实现。
1 | @Service |
UrlFilterServiceImpl 在进行新增、修改、删除时会调用 initFilterChain 来重新初始化 Shiro 的 URL 拦截器链,即同步数据库中的 URL 拦截器定义到 Shiro 中。此处也要注意如果直接修改数据库是不会起作用的,因为只要调用这几个 Service 方法时才同步。另外当容器启动时会自动回调 initFilterChain 来完成容器启动后的 URL 拦截器的注册。
ShiroFilerChainManager
1 | @Service |
1、init:Spring 容器启动时会调用 init 方法把在 spring 配置文件中配置的默认拦截器保存下来,之后会自动与数据库中的配置进行合并。
2、initFilterChains:UrlFilterServiceImpl 会在 Spring 容器启动或进行增删改 UrlFilter 时进行注册 URL 拦截器到 Shiro。
拦截器及拦截器链知识请参考《第八章 拦截器机制》,此处再介绍下 Shiro 拦截器的流程:
1 | AbstractShiroFilter //如ShiroFilter/ SpringShiroFilter都继承该Filter |
默认情况下如使用 ShiroFilterFactoryBean 创建 shiroFilter 时,默认使用 PathMatchingFilterChainResolver 进行解析,而它默认是根据当前请求的 URL 获取相应的拦截器链,使用 Ant 模式进行 URL 匹配;默认使用 DefaultFilterChainManager 进行拦截器链的管理。
PathMatchingFilterChainResolver 默认流程:
1 | public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) { |
默认实现有点小问题:
如果多个拦截器链都匹配了当前请求 URL,那么只返回第一个找到的拦截器链;后续我们可以修改此处的代码,将多个匹配的拦截器链合并返回。
DefaultFilterChainManager 内部使用 Map 来管理 URL 模式 - 拦截器链的关系;也就是说相同的 URL 模式只能定义一个拦截器链,不能重复定义;而且如果多个拦截器链都匹配时是无序的(因为使用 map.keySet() 获取拦截器链的名字,即 URL 模式)。
FilterChainManager 接口:
1 | public interface FilterChainManager { |
此接口主要三个功能:注册拦截器,注册拦截器链,对原始拦截器链生成代理之后的拦截器链,比如
1 | <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> |
filters 属性定义了拦截器;filterChainDefinitions 定义了拦截器链;如 /** 就是拦截器链的名字;而 user,sysUser 就是拦截器名字列表。
之前说过默认的 PathMatchingFilterChainResolver 和 DefaultFilterChainManager 不能满足我们的需求,我们稍微扩展了一下:
CustomPathMatchingFilterChainResolver
1 | public class CustomPathMatchingFilterChainResolver |
和默认的 PathMatchingFilterChainResolver 区别是,此处得到所有匹配的拦截器链,然后通过调用 CustomDefaultFilterChainManager.proxy(originalChain, chainNames) 进行合并后代理。
CustomDefaultFilterChainManager
1 | public class CustomDefaultFilterChainManager extends DefaultFilterChainManager { |
- CustomDefaultFilterChainManager:调用其构造器时,会自动注册默认的拦截器;
- loginUrl、successUrl、unauthorizedUrl:分别对应登录地址、登录成功后默认跳转地址、未授权跳转地址,用于给相应拦截器的;
- filterChainDefinitionMap:用于存储如 ShiroFilterFactoryBean 在配置文件中配置的拦截器链定义,即可以认为是默认的静态拦截器链;会自动与数据库中加载的合并;
- setDefaultFilterChainDefinitions:解析配置文件中传入的字符串拦截器链配置,解析为相应的拦截器链;
- setCustomFilters:注册我们自定义的拦截器;如 ShiroFilterFactoryBean 的 filters 属性;
- init:初始化方法,Spring 容器启动时会调用,首先其会自动给相应的拦截器设置如 loginUrl、successUrl、unauthorizedUrl;其次根据 filterChainDefinitionMap 构建默认的拦截器链;
- initFilter:此处我们忽略实现 initFilter,因为交给 spring 管理了,所以 Filter 的相关配置会在 Spring 配置中完成;
- proxy:组合多个拦截器链为一个生成一个新的 FilterChain 代理。
Web 层控制器
请参考 com.github.zhangkaitao.shiro.chapter19.web.controller 包,相对于第十六章添加了 UrlFilterController 用于 UrlFilter 的维护。另外,移除了控制器方法上的权限注解,而是使用动态 URL 拦截进行控制。
Spring 配置——spring-config-shiro.xml
1 | <bean id="filterChainManager" |
filterChainManager 是我们自定义的 CustomDefaultFilterChainManager,注册相应的拦截器及默认的拦截器链。
1 | <bean id="filterChainResolver" |
filterChainResolver 是自定义的 CustomPathMatchingFilterChainResolver,使用上边的 filterChainManager 进行拦截器链的管理。
1 | <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> |
shiroFilter 不再定义 filters 及 filterChainDefinitions,而是交给了 filterChainManager 进行完成。
1 | <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> |
最后把 filterChainResolver 注册给 shiroFilter,其使用它进行动态 URL 权限控制。
其他配置和第十六章一样,请参考第十六章。
测试
1、首先执行 shiro-data.sql 初始化数据。
2、然后再 URL 管理中新增如下数据:
3、访问 http://localhost:8080/chapter19/user
时要求用户拥有 aa 角色,此时是没有的所以会跳转到未授权页面;
4、添加 aa 角色然后授权给用户,此时就有权限访问 http://localhost:8080/chapter19/user
。
实际项目可以在此基础上进行扩展。