用過(guò) Spring Security 的朋友應(yīng)該比較熟悉對(duì) URL 進(jìn)行全局的權(quán)限控制,即訪問(wèn) URL 時(shí)進(jìn)行權(quán)限匹配;如果沒(méi)有權(quán)限直接跳到相應(yīng)的錯(cuò)誤頁(yè)面。Shiro 也支持類似的機(jī)制,不過(guò)需要稍微改造下來(lái)滿足實(shí)際需求。不過(guò)在 Shiro 中,更多的是通過(guò) AOP 進(jìn)行分散的權(quán)限控制,即方法級(jí)別的;而通過(guò) URL 進(jìn)行權(quán)限控制是一種集中的權(quán)限控制。本章將介紹如何在 Shiro 中完成動(dòng)態(tài) URL 權(quán)限控制。
本章代碼基于《第十六章 綜合實(shí)例》,請(qǐng)先了解相關(guān)數(shù)據(jù)模型及基本流程后再學(xué)習(xí)本章。
表及數(shù)據(jù) SQL
請(qǐng)運(yùn)行 shiro-example-chapter19/sql/ shiro-schema.sql 表結(jié)構(gòu)
請(qǐng)運(yùn)行 shiro-example-chapter19/sql/ shiro-schema.sql 數(shù)據(jù)
實(shí)體
具體請(qǐng)參考 com.github.zhangkaitao.shiro.chapter19 包下的實(shí)體。
public class UrlFilter implements Serializable {
private Long id;
private String name; //url名稱/描述
private String url; //地址
private String roles; //所需要的角色,可省略
private String permissions; //所需要的權(quán)限,可省略
}
表示攔截的 URL 和角色 / 權(quán)限之間的關(guān)系,多個(gè)角色 / 權(quán)限之間通過(guò)逗號(hào)分隔,此處還可以擴(kuò)展其他的關(guān)系,另外可以加如 available 屬性表示是否開(kāi)啟該攔截。
DAO
具體請(qǐng)參考 com.github.zhangkaitao.shiro.chapter19.dao 包下的 DAO 接口及實(shí)現(xiàn)。
Service
具體請(qǐng)參考 com.github.zhangkaitao.shiro.chapter19.service 包下的 Service 接口及實(shí)現(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 攔截的增刪改查實(shí)現(xiàn)。
@Service
public class UrlFilterServiceImpl implements UrlFilterService {
@Autowired
private ShiroFilerChainManager shiroFilerChainManager;
@Override
public UrlFilter createUrlFilter(UrlFilter urlFilter) {
urlFilterDao.createUrlFilter(urlFilter);
initFilterChain();
return urlFilter;
}
//其他方法請(qǐng)參考源碼
@PostConstruct
public void initFilterChain() {
shiroFilerChainManager.initFilterChains(findAll());
}
}
UrlFilterServiceImpl 在進(jìn)行新增、修改、刪除時(shí)會(huì)調(diào)用 initFilterChain 來(lái)重新初始化 Shiro 的 URL 攔截器鏈,即同步數(shù)據(jù)庫(kù)中的 URL 攔截器定義到 Shiro 中。此處也要注意如果直接修改數(shù)據(jù)庫(kù)是不會(huì)起作用的,因?yàn)橹灰{(diào)用這幾個(gè) Service 方法時(shí)才同步。另外當(dāng)容器啟動(dòng)時(shí)會(huì)自動(dòng)回調(diào) initFilterChain 來(lái)完成容器啟動(dòng)后的 URL 攔截器的注冊(cè)。
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并注冊(cè)默認(rèn)的
filterChainManager.getFilterChains().clear();
if(defaultFilterChains != null) {
filterChainManager.getFilterChains().putAll(defaultFilterChains);
}
//2、循環(huán)URL Filter 注冊(cè)filter chain
for (UrlFilter urlFilter : urlFilters) {
String url = urlFilter.getUrl();
//注冊(cè)roles filter
if (!StringUtils.isEmpty(urlFilter.getRoles())) {
filterChainManager.addToChain(url, "roles", urlFilter.getRoles());
}
//注冊(cè)perms filter
if (!StringUtils.isEmpty(urlFilter.getPermissions())) {
filterChainManager.addToChain(url, "perms", urlFilter.getPermissions());
}
}
}
}
1、init:Spring 容器啟動(dòng)時(shí)會(huì)調(diào)用 init 方法把在 spring 配置文件中配置的默認(rèn)攔截器保存下來(lái),之后會(huì)自動(dòng)與數(shù)據(jù)庫(kù)中的配置進(jìn)行合并。
2、initFilterChains:UrlFilterServiceImpl 會(huì)在 Spring 容器啟動(dòng)或進(jìn)行增刪改 UrlFilter 時(shí)進(jìn)行注冊(cè) URL 攔截器到 Shiro。
攔截器及攔截器鏈知識(shí)請(qǐng)參考《第八章 攔截器機(jī)制》,此處再介紹下 Shiro 攔截器的流程:
AbstractShiroFilter //如ShiroFilter/ SpringShiroFilter都繼承該Filter
doFilter //Filter的doFilter
doFilterInternal //轉(zhuǎn)調(diào)doFilterInternal
executeChain(request, response, chain) //執(zhí)行攔截器鏈
FilterChain chain = getExecutionChain(request, response, origChain) //使用原始攔截器鏈獲取新的攔截器鏈
chain.doFilter(request, response) //執(zhí)行新組裝的攔截器鏈
getExecutionChain(request, response, origChain) //獲取攔截器鏈流程
FilterChainResolver resolver = getFilterChainResolver(); //獲取相應(yīng)的FilterChainResolver
FilterChain resolved = resolver.getChain(request, response, origChain); //通過(guò)FilterChainResolver根據(jù)當(dāng)前請(qǐng)求解析到新的FilterChain攔截器鏈
默認(rèn)情況下如使用 ShiroFilterFactoryBean 創(chuàng)建 shiroFilter 時(shí),默認(rèn)使用 PathMatchingFilterChainResolver 進(jìn)行解析,而它默認(rèn)是根據(jù)當(dāng)前請(qǐng)求的 URL 獲取相應(yīng)的攔截器鏈,使用 Ant 模式進(jìn)行 URL 匹配;默認(rèn)使用 DefaultFilterChainManager 進(jìn)行攔截器鏈的管理。
PathMatchingFilterChainResolver 默認(rèn)流程:
public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
//1、首先獲取攔截器鏈管理器
FilterChainManager filterChainManager = getFilterChainManager();
if (!filterChainManager.hasChains()) {
return null;
}
//2、接著獲取當(dāng)前請(qǐng)求的URL(不帶上下文)
String requestURI = getPathWithinApplication(request);
//3、循環(huán)攔截器管理器中的攔截器定義(攔截器鏈的名字就是URL模式)
for (String pathPattern : filterChainManager.getChainNames()) {
//4、如當(dāng)前URL匹配攔截器名字(URL模式)
if (pathMatches(pathPattern, requestURI)) {
//5、返回該URL模式定義的攔截器鏈
return filterChainManager.proxy(originalChain, pathPattern);
}
}
return null;
}
默認(rèn)實(shí)現(xiàn)有點(diǎn)小問(wèn)題:
如果多個(gè)攔截器鏈都匹配了當(dāng)前請(qǐng)求 URL,那么只返回第一個(gè)找到的攔截器鏈;后續(xù)我們可以修改此處的代碼,將多個(gè)匹配的攔截器鏈合并返回。
DefaultFilterChainManager 內(nèi)部使用 Map 來(lái)管理 URL 模式 - 攔截器鏈的關(guān)系;也就是說(shuō)相同的 URL 模式只能定義一個(gè)攔截器鏈,不能重復(fù)定義;而且如果多個(gè)攔截器鏈都匹配時(shí)是無(wú)序的(因?yàn)槭褂?map.keySet() 獲取攔截器鏈的名字,即 URL 模式)。
FilterChainManager 接口:
public interface FilterChainManager {
Map<String, Filter> getFilters(); //得到注冊(cè)的攔截器
void addFilter(String name, Filter filter); //注冊(cè)攔截器
void addFilter(String name, Filter filter, boolean init); //注冊(cè)攔截器
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); //使用指定的攔截器鏈代理原始攔截器鏈
}
此接口主要三個(gè)功能:注冊(cè)攔截器,注冊(cè)攔截器鏈,對(duì)原始攔截器鏈生成代理之后的攔截器鏈,比如
<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 就是攔截器名字列表。
之前說(shuō)過(guò)默認(rèn)的 PathMatchingFilterChainResolver 和 DefaultFilterChainManager 不能滿足我們的需求,我們稍微擴(kuò)展了一下:
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);
}
}
和默認(rèn)的 PathMatchingFilterChainResolver 區(qū)別是,此處得到所有匹配的攔截器鏈,然后通過(guò)調(diào)用 CustomDefaultFilterChainManager.proxy(originalChain, chainNames) 進(jìn)行合并后代理。
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) {
//請(qǐng)參考源碼
}
private void applySuccessUrlIfNecessary(Filter filter) {
//請(qǐng)參考源碼
}
private void applyUnauthorizedUrlIfNecessary(Filter filter) {
//請(qǐng)參考源碼
}
}
Web 層控制器
請(qǐng)參考 com.github.zhangkaitao.shiro.chapter19.web.controller 包,相對(duì)于第十六章添加了 UrlFilterController 用于 UrlFilter 的維護(hù)。另外,移除了控制器方法上的權(quán)限注解,而是使用動(dòng)態(tài) URL 攔截進(jìn)行控制。
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>
filterChainManager 是我們自定義的 CustomDefaultFilterChainManager,注冊(cè)相應(yīng)的攔截器及默認(rèn)的攔截器鏈。
<bean id="filterChainResolver"
class="com.github.zhangkaitao.shiro.spring.CustomPathMatchingFilterChainResolver">
<property name="customDefaultFilterChainManager" ref="filterChainManager"/>
</bean>
filterChainResolver 是自定義的 CustomPathMatchingFilterChainResolver,使用上邊的 filterChainManager 進(jìn)行攔截器鏈的管理。
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
</bean>
shiroFilter 不再定義 filters 及 filterChainDefinitions,而是交給了 filterChainManager 進(jìn)行完成。
<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 注冊(cè)給 shiroFilter,其使用它進(jìn)行動(dòng)態(tài) URL 權(quán)限控制。
其他配置和第十六章一樣,請(qǐng)參考第十六章。
測(cè)試
1、首先執(zhí)行 shiro-data.sql 初始化數(shù)據(jù)。
2、然后再 URL 管理中新增如下數(shù)據(jù):
3、訪問(wèn) http://localhost:8080/chapter19/user
時(shí)要求用戶擁有 aa 角色,此時(shí)是沒(méi)有的所以會(huì)跳轉(zhuǎn)到未授權(quán)頁(yè)面;
4、添加 aa 角色然后授權(quán)給用戶,此時(shí)就有權(quán)限訪問(wèn) http://localhost:8080/chapter19/user
。
實(shí)際項(xiàng)目可以在此基礎(chǔ)上進(jìn)行擴(kuò)展。
更多建議: