鍍金池/ 教程/ Java/ 動態(tài) URL 權限控制
綜合實例
JSP 標簽
集成驗證碼
在線會話管理
身份驗證
攔截器機制
編碼/加密
INI 配置
單點登錄
并發(fā)登錄人數(shù)控制
OAuth2 集成
動態(tài) URL 權限控制
Realm 及相關對象
多項目集中權限管理及分布式會話
授予身份及切換身份
RememberMe
會話管理
與 Spring集成
與 Web 集成
緩存機制
簡介
授權
SSL
無狀態(tài) Web 應用集成

動態(tài) URL 權限控制

用過 Spring Security 的朋友應該比較熟悉對 URL 進行全局的權限控制,即訪問 URL 時進行權限匹配;如果沒有權限直接跳到相應的錯誤頁面。Shiro 也支持類似的機制,不過需要稍微改造下來滿足實際需求。不過在 Shiro 中,更多的是通過 AOP 進行分散的權限控制,即方法級別的;而通過 URL 進行權限控制是一種集中的權限控制。本章將介紹如何在 Shiro 中完成動態(tài) URL 權限控制。

本章代碼基于《第十六章 綜合實例》,請先了解相關數(shù)據(jù)模型及基本流程后再學習本章。

表及數(shù)據(jù) SQL

請運行 shiro-example-chapter19/sql/ shiro-schema.sql 表結構
請運行 shiro-example-chapter19/sql/ shiro-schema.sql 數(shù)據(jù)

實體

具體請參考 com.github.zhangkaitao.shiro.chapter19 包下的實體。

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 接口及實現(xiàn)。

Service

具體請參考 com.github.zhangkaitao.shiro.chapter19.service 包下的 Service 接口及實現(xiàn)。

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 攔截的增刪改查實現(xiàn)。

@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());
    }
}&nbsp;

UrlFilterServiceImpl 在進行新增、修改、刪除時會調用 initFilterChain 來重新初始化 Shiro 的 URL 攔截器鏈,即同步數(shù)據(jù)庫中的 URL 攔截器定義到 Shiro 中。此處也要注意如果直接修改數(shù)據(jù)庫是不會起作用的,因為只要調用這幾個 Service 方法時才同步。另外當容器啟動時會自動回調 initFilterChain 來完成容器啟動后的 URL 攔截器的注冊。

ShiroFilerChainManager

@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、循環(huán)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());
            }
        }
    }
}&nbsp;

1、init:Spring 容器啟動時會調用 init 方法把在 spring 配置文件中配置的默認攔截器保存下來,之后會自動與數(shù)據(jù)庫中的配置進行合并。
2、initFilterChains:UrlFilterServiceImpl 會在 Spring 容器啟動或進行增刪改 UrlFilter 時進行注冊 URL 攔截器到 Shiro。

攔截器及攔截器鏈知識請參考《第八章 攔截器機制》,此處再介紹下 Shiro 攔截器的流程:

AbstractShiroFilter //如ShiroFilter/ SpringShiroFilter都繼承該Filter
   doFilter //Filter的doFilter
     doFilterInternal //轉調doFilterInternal
       executeChain(request, response, chain) //執(zhí)行攔截器鏈
         FilterChain chain = getExecutionChain(request, response, origChain) //使用原始攔截器鏈獲取新的攔截器鏈
           chain.doFilter(request, response) //執(zhí)行新組裝的攔截器鏈

getExecutionChain(request, response, origChain) //獲取攔截器鏈流程
       FilterChainResolver resolver = getFilterChainResolver(); //獲取相應的FilterChainResolver
       FilterChain resolved = resolver.getChain(request, response, origChain); //通過FilterChainResolver根據(jù)當前請求解析到新的FilterChain攔截器鏈

默認情況下如使用 ShiroFilterFactoryBean 創(chuàng)建 shiroFilter 時,默認使用 PathMatchingFilterChainResolver 進行解析,而它默認是根據(jù)當前請求的 URL 獲取相應的攔截器鏈,使用 Ant 模式進行 URL 匹配;默認使用 DefaultFilterChainManager 進行攔截器鏈的管理。

PathMatchingFilterChainResolver 默認流程:

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、循環(huán)攔截器管理器中的攔截器定義(攔截器鏈的名字就是URL模式)
    for (String pathPattern : filterChainManager.getChainNames()) {
        //4、如當前URL匹配攔截器名字(URL模式)
        if (pathMatches(pathPattern, requestURI)) {
            //5、返回該URL模式定義的攔截器鏈
            return filterChainManager.proxy(originalChain, pathPattern);
        }
    }
    return null;
}&nbsp;

默認實現(xiàn)有點小問題:

如果多個攔截器鏈都匹配了當前請求 URL,那么只返回第一個找到的攔截器鏈;后續(xù)我們可以修改此處的代碼,將多個匹配的攔截器鏈合并返回。

DefaultFilterChainManager 內(nèi)部使用 Map 來管理 URL 模式 - 攔截器鏈的關系;也就是說相同的 URL 模式只能定義一個攔截器鏈,不能重復定義;而且如果多個攔截器鏈都匹配時是無序的(因為使用 map.keySet() 獲取攔截器鏈的名字,即 URL 模式)。

FilterChainManager 接口:

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); //根據(jù)攔截器鏈定義創(chuàng)建攔截器鏈
    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); //使用指定的攔截器鏈代理原始攔截器鏈
}&nbsp;

此接口主要三個功能:注冊攔截器,注冊攔截器鏈,對原始攔截器鏈生成代理之后的攔截器鏈,比如

<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>&nbsp;

filters 屬性定義了攔截器;filterChainDefinitions 定義了攔截器鏈;如 /** 就是攔截器鏈的名字;而 user,sysUser 就是攔截器名字列表。

之前說過默認的 PathMatchingFilterChainResolver 和 DefaultFilterChainManager 不能滿足我們的需求,我們稍微擴展了一下:

CustomPathMatchingFilterChainResolver

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);
  }
}&nbsp;

和默認的 PathMatchingFilterChainResolver 區(qū)別是,此處得到所有匹配的攔截器鏈,然后通過調用 CustomDefaultFilterChainManager.proxy(originalChain, chainNames) 進行合并后代理。

CustomDefaultFilterChainManager

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) {
        //請參考源碼
    }
}&nbsp;
  1. CustomDefaultFilterChainManager:調用其構造器時,會自動注冊默認的攔截器;
  2. loginUrl、successUrl、unauthorizedUrl:分別對應登錄地址、登錄成功后默認跳轉地址、未授權跳轉地址,用于給相應攔截器的;
  3. filterChainDefinitionMap:用于存儲如 ShiroFilterFactoryBean 在配置文件中配置的攔截器鏈定義,即可以認為是默認的靜態(tài)攔截器鏈;會自動與數(shù)據(jù)庫中加載的合并;
  4. setDefaultFilterChainDefinitions:解析配置文件中傳入的字符串攔截器鏈配置,解析為相應的攔截器鏈;
  5. setCustomFilters:注冊我們自定義的攔截器;如 ShiroFilterFactoryBean 的 filters 屬性;
  6. init:初始化方法,Spring 容器啟動時會調用,首先其會自動給相應的攔截器設置如 loginUrl、successUrl、unauthorizedUrl;其次根據(jù) filterChainDefinitionMap 構建默認的攔截器鏈;
  7. initFilter:此處我們忽略實現(xiàn) initFilter,因為交給 spring 管理了,所以 Filter 的相關配置會在 Spring 配置中完成;
  8. proxy:組合多個攔截器鏈為一個生成一個新的 FilterChain 代理。

Web 層控制器

請參考 com.github.zhangkaitao.shiro.chapter19.web.controller 包,相對于第十六章添加了 UrlFilterController 用于 UrlFilter 的維護。另外,移除了控制器方法上的權限注解,而是使用動態(tài) URL 攔截進行控制。

Spring 配置——spring-config-shiro.xml

<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>&nbsp;

filterChainManager 是我們自定義的 CustomDefaultFilterChainManager,注冊相應的攔截器及默認的攔截器鏈。

<bean id="filterChainResolver" 
    class="com.github.zhangkaitao.shiro.spring.CustomPathMatchingFilterChainResolver">
    <property name="customDefaultFilterChainManager" ref="filterChainManager"/>
</bean>&nbsp;

filterChainResolver 是自定義的 CustomPathMatchingFilterChainResolver,使用上邊的 filterChainManager 進行攔截器鏈的管理。

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

shiroFilter 不再定義 filters 及 filterChainDefinitions,而是交給了 filterChainManager 進行完成。

<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    <property name="targetObject" ref="shiroFilter"/>
    <property name="targetMethod" value="setFilterChainResolver"/>
    <property name="arguments" ref="filterChainResolver"/>
</bean>&nbsp;

最后把 filterChainResolver 注冊給 shiroFilter,其使用它進行動態(tài) URL 權限控制。

其他配置和第十六章一樣,請參考第十六章。

測試

1、首先執(zhí)行 shiro-data.sql 初始化數(shù)據(jù)。
2、然后再 URL 管理中新增如下數(shù)據(jù):

http://wiki.jikexueyuan.com/project/shiro/images/29.png" alt="" />

3、訪問 http://localhost:8080/chapter19/user 時要求用戶擁有 aa 角色,此時是沒有的所以會跳轉到未授權頁面;
4、添加 aa 角色然后授權給用戶,此時就有權限訪問 http://localhost:8080/chapter19/user。

實際項目可以在此基礎上進行擴展。

上一篇:授權下一篇:RememberMe