Shiro學習(19)動態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包下的實體。
Java程式碼- public class UrlFilter implements Serializable {
- private Long id;
- private String name; //url名稱/描述
- private String url; //地址
- private String roles; //所需要的角色,可省略
- private String permissions; //所需要的許可權,可省略
- }
表示攔截的URL和角色/許可權之間的關係,多個角色/許可權之間通過逗號分隔,此處還可以擴充套件其他的關係,另外可以加如available屬性表示是否開啟該攔截。
DAO
具體請參考com.github.zhangkaitao.shiro.chapter19.dao包下的DAO介面及實現。
Service
具體請參考com.github.zhangkaitao.shiro.chapter19.service包下的Service介面及實現。
Java程式碼- public interface UrlFilterService {
- public UrlFilter createUrlFilter(UrlFilter urlFilter);
- public UrlFilter updateUrlFilter(UrlFilter urlFilter);
- public void deleteUrlFilter(Long urlFilterId);
- public UrlFilter findOne(Long urlFilterId);
- public List<UrlFilter> findAll();
- }
基本的URL攔截的增刪改查實現。
Java程式碼- @Service
- public class UrlFilterServiceImpl implements UrlFilterService {
- @Autowired
- private ShiroFilerChainManager shiroFilerChainManager;
- @Override
- public UrlFilter createUrlFilter(UrlFilter urlFilter) {
- urlFilterDao.createUrlFilter(urlFilter);
- initFilterChain();
- return urlFilter;
- }
- //其他方法請參考原始碼
- @PostConstruct
- public void initFilterChain() {
- shiroFilerChainManager.initFilterChains(findAll());
- }
- }
UrlFilterServiceImpl在進行新增、修改、刪除時會呼叫initFilterChain來重新初始化Shiro的URL攔截器鏈,即同步資料庫中的URL攔截器定義到Shiro中。此處也要注意如果直接修改資料庫是不會起作用的,因為只要呼叫這幾個Service方法時才同步。另外當容器啟動時會自動回撥initFilterChain來完成容器啟動後的URL攔截器的註冊。
ShiroFilerChainManager
Java程式碼- @Service
- public class ShiroFilerChainManager {
- @Autowired private DefaultFilterChainManager filterChainManager;
- private Map<String, NamedFilterList> defaultFilterChains;
- @PostConstruct
- public void init() {
- defaultFilterChains =
- new HashMap<String, NamedFilterList>(filterChainManager.getFilterChains());
- }
- public void initFilterChains(List<UrlFilter> urlFilters) {
- //1、首先刪除以前老的filter chain並註冊預設的
- filterChainManager.getFilterChains().clear();
- if(defaultFilterChains != null) {
- filterChainManager.getFilterChains().putAll(defaultFilterChains);
- }
- //2、迴圈URL Filter 註冊filter chain
- 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());
- }
- }
- }
- }
1、init:Spring容器啟動時會呼叫init方法把在spring配置檔案中配置的預設攔截器儲存下來,之後會自動與資料庫中的配置進行合併。
2、initFilterChains:UrlFilterServiceImpl會在Spring容器啟動或進行增刪改UrlFilter時進行註冊URL攔截器到Shiro。
攔截器及攔截器鏈知識請參考《第八章 攔截器機制》,此處再介紹下Shiro攔截器的流程:
AbstractShiroFilter //如ShiroFilter/ SpringShiroFilter都繼承該Filter
doFilter //Filter的doFilter
doFilterInternal //轉調doFilterInternal
executeChain(request, response, chain) //執行攔截器鏈
FilterChain chain = getExecutionChain(request, response, origChain) //使用原始攔截器鏈獲取新的攔截器鏈
chain.doFilter(request, response) //執行新組裝的攔截器鏈
getExecutionChain(request, response, origChain) //獲取攔截器鏈流程
FilterChainResolver resolver = getFilterChainResolver(); //獲取相應的FilterChainResolver
FilterChain resolved = resolver.getChain(request, response, origChain); //通過FilterChainResolver根據當前請求解析到新的FilterChain攔截器鏈
預設情況下如使用ShiroFilterFactoryBean建立shiroFilter時,預設使用PathMatchingFilterChainResolver進行解析,而它預設是根據當前請求的URL獲取相應的攔截器鏈,使用Ant模式進行URL匹配;預設使用DefaultFilterChainManager進行攔截器鏈的管理。
PathMatchingFilterChainResolver預設流程:
Java程式碼- 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、迴圈攔截器管理器中的攔截器定義(攔截器鏈的名字就是URL模式)
- for (String pathPattern : filterChainManager.getChainNames()) {
- //4、如當前URL匹配攔截器名字(URL模式)
- if (pathMatches(pathPattern, requestURI)) {
- //5、返回該URL模式定義的攔截器鏈
- return filterChainManager.proxy(originalChain, pathPattern);
- }
- }
- return null;
- }
預設實現有點小問題:
如果多個攔截器鏈都匹配了當前請求URL,那麼只返回第一個找到的攔截器鏈;後續我們可以修改此處的程式碼,將多個匹配的攔截器鏈合併返回。
DefaultFilterChainManager內部使用Map來管理URL模式-攔截器鏈的關係;也就是說相同的URL模式只能定義一個攔截器鏈,不能重複定義;而且如果多個攔截器鏈都匹配時是無序的(因為使用map.keySet()獲取攔截器鏈的名字,即URL模式)。
FilterChainManager介面:
Java程式碼- 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); //使用指定的攔截器鏈代理原始攔截器鏈
- }
此介面主要三個功能:註冊攔截器,註冊攔截器鏈,對原始攔截器鏈生成代理之後的攔截器鏈,比如
Java程式碼- <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>
filters屬性定義了攔截器;filterChainDefinitions定義了攔截器鏈;如/**就是攔截器鏈的名字;而user,sysUser就是攔截器名字列表。
之前說過預設的PathMatchingFilterChainResolver和DefaultFilterChainManager不能滿足我們的需求,我們稍微擴充套件了一下:
CustomPathMatchingFilterChainResolver
Java程式碼- public class CustomPathMatchingFilterChainResolver
- extends PathMatchingFilterChainResolver {
- private CustomDefaultFilterChainManager customDefaultFilterChainManager;
- public void setCustomDefaultFilterChainManager(
- CustomDefaultFilterChainManager customDefaultFilterChainManager) {
- this.customDefaultFilterChainManager = customDefaultFilterChainManager;
- setFilterChainManager(customDefaultFilterChainManager);
- }
- public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
- FilterChainManager filterChainManager = getFilterChainManager();
- if (!filterChainManager.hasChains()) {
- return null;
- }
- String requestURI = getPathWithinApplication(request);
- List<String> chainNames = new ArrayList<String>();
- for (String pathPattern : filterChainManager.getChainNames()) {
- if (pathMatches(pathPattern, requestURI)) {
- chainNames.add(pathPattern);
- }
- }
- if(chainNames.size() == 0) {
- return null;
- }
- return customDefaultFilterChainManager.proxy(originalChain, chainNames);
- }
- }
和預設的PathMatchingFilterChainResolver區別是,此處得到所有匹配的攔截器鏈,然後通過呼叫CustomDefaultFilterChainManager.proxy(originalChain, chainNames)進行合併後代理。
CustomDefaultFilterChainManager
Java程式碼- public class CustomDefaultFilterChainManager extends DefaultFilterChainManager {
- 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);
- }
- public Map<String, String> getFilterChainDefinitionMap() {
- return filterChainDefinitionMap;
- }
- public void setFilterChainDefinitionMap(Map<String, String> filterChainDefinitionMap) {
- this.filterChainDefinitionMap = filterChainDefinitionMap;
- }
- public void setCustomFilters(Map<String, Filter> customFilters) {
- for(Map.Entry<String, Filter> entry : customFilters.entrySet()) {
- addFilter(entry.getKey(), entry.getValue(), false);
- }
- }
- 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);
- }
- public String getLoginUrl() {
- return loginUrl;
- }
- public void setLoginUrl(String loginUrl) {
- this.loginUrl = loginUrl;
- }
- public String getSuccessUrl() {
- return successUrl;
- }
- public void setSuccessUrl(String successUrl) {
- this.successUrl = successUrl;
- }
- public String getUnauthorizedUrl() {
- return unauthorizedUrl;
- }
- public void setUnauthorizedUrl(String unauthorizedUrl) {
- this.unauthorizedUrl = unauthorizedUrl;
- }
- @PostConstruct
- public void init() {
- 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);
- }
- 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();
- createChain(url, chainDefinition);
- }
- }
- }
- protected void initFilter(Filter filter) {
- //ignore
- }
- public FilterChain proxy(FilterChain original, List<String> chainNames) {
- 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) {
- //請參考原始碼
- }
- private void applySuccessUrlIfNecessary(Filter filter) {
- //請參考原始碼
- }
- private void applyUnauthorizedUrlIfNecessary(Filter filter) {
- //請參考原始碼
- }
- }
1、CustomDefaultFilterChainManager:呼叫其構造器時,會自動註冊預設的攔截器;
2、loginUrl、successUrl、unauthorizedUrl:分別對應登入地址、登入成功後預設跳轉地址、未授權跳轉地址,用於給相應攔截器的;
3、filterChainDefinitionMap:用於儲存如ShiroFilterFactoryBean在配置檔案中配置的攔截器鏈定義,即可以認為是預設的靜態攔截器鏈;會自動與資料庫中載入的合併;
4、setDefaultFilterChainDefinitions:解析配置檔案中傳入的字串攔截器鏈配置,解析為相應的攔截器鏈;
5、setCustomFilters:註冊我們自定義的攔截器;如ShiroFilterFactoryBean的filters屬性;
6、init:初始化方法,Spring容器啟動時會呼叫,首先其會自動給相應的攔截器設定如loginUrl、successUrl、unauthorizedUrl;其次根據filterChainDefinitionMap構建預設的攔截器鏈;
7、initFilter:此處我們忽略實現initFilter,因為交給spring管理了,所以Filter的相關配置會在Spring配置中完成;
8、proxy:組合多個攔截器鏈為一個生成一個新的FilterChain代理。
Web層控制器
請參考com.github.zhangkaitao.shiro.chapter19.web.controller包,相對於第十六章添加了UrlFilterController用於UrlFilter的維護。另外,移除了控制器方法上的許可權註解,而是使用動態URL攔截進行控制。
Spring配置——spring-config-shiro.xml
Java程式碼- <bean id="filterChainManager"
- class="com.github.zhangkaitao.shiro.spring.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="defaultFilterChainDefinitions">
- <value>
- /login = authc
- /logout = logout
- /unauthorized.jsp = authc
- /** = user,sysUser
- </value>
- </property>
- </bean>
filterChainManager是我們自定義的CustomDefaultFilterChainManager,註冊相應的攔截器及預設的攔截器鏈。
Java程式碼- <bean id="filterChainResolver"
- class="com.github.zhangkaitao.shiro.spring.CustomPathMatchingFilterChainResolver">
- <property name="customDefaultFilterChainManager" ref="filterChainManager"/>
- </bean>
filterChainResolver是自定義的CustomPathMatchingFilterChainResolver,使用上邊的filterChainManager進行攔截器鏈的管理。
Java程式碼- <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
- <property name="securityManager" ref="securityManager"/>
- </bean>
shiroFilter不再定義filters及filterChainDefinitions,而是交給了filterChainManager進行完成。
Java程式碼- <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
- <property name="targetObject" ref="shiroFilter"/>
- <property name="targetMethod" value="setFilterChainResolver"/>
- <property name="arguments" ref="filterChainResolver"/>
- </bean>
最後把filterChainResolver註冊給shiroFilter,其使用它進行動態URL許可權控制。
其他配置和第十六章一樣,請參考第十六章。
測試
1、首先執行shiro-data.sql初始化資料。
2、然後再URL管理中新增如下資料:
3、訪問http://localhost:8080/chapter19/user時要求使用者擁有aa角色,此時是沒有的所以會跳轉到未授權頁面;
4、新增aa角色然後授權給使用者,此時就有許可權訪問http://localhost:8080/chapter19/user。
實際專案可以在此基礎上進行擴充套件。