ShiroFilterFactoryBean原始碼及阻截原理深入分析
Shiro提供了與Web整合的支援,其通過一個ShiroFilter
入口來攔截需要安全控制的URL,然後進行相應的控制,ShiroFilter類似於如Strut2/SpringMVC這種web框架的前端控制器,其是安全控制的入口點,其負責讀取配置(如ini配置檔案),然後判斷URL是否需要登入/許可權等工作。
而要在Spring
中使用Shiro
的話,可在web.xml
中配置一個DelegatingFilterProxy
,DelegatingFilterProxy
作用是自動到Spring
容器查詢名字為shiroFilter
(filter-name
)的bean
並把所有Filter
的操作委託給它。
首先是在web.xml
DelegatingFilterProxy
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value >true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
配置好DelegatingFilterProxy
後,下面只要再把ShiroFilter
配置到Spring
容器(此處為Spring
的配置檔案)即可:
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
</bean>
可以看到我們使用了ShiroFilterFactoryBean
來建立shiroFilter
,這裡用到了Spring
中一種特殊的Bean——FactoryBean
。當需要得到名為”shiroFilter“的bean時,會呼叫其getObject()
來獲取例項。下面我們通過分析ShiroFilterFactoryBean
建立例項的過程來探究Shiro是如何實現安全攔截的:
public Object getObject() throws Exception {
if (instance == null) {
instance = createInstance();
}
return instance;
}
其中呼叫了createInstance()
來建立例項:
protected AbstractShiroFilter createInstance() throws Exception {
// 這裡是通過FactoryBean注入的SecurityManager(必須)
SecurityManager securityManager = getSecurityManager();
if (securityManager == null) {
String msg = "SecurityManager property must be set.";
throw new BeanInitializationException(msg);
}
if (!(securityManager instanceof WebSecurityManager)) {
String msg = "The security manager does not implement the WebSecurityManager interface.";
throw new BeanInitializationException(msg);
}
FilterChainManager manager = createFilterChainManager();
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
chainResolver.setFilterChainManager(manager);
return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
}
可以看到建立SpringShiroFilter
時用到了兩個元件:SecurityManager
和ChainResolver
。
-
SecurityManager
:我們知道其在Shiro中的地位,類似於一個“安全大管家”,相當於SpringMVC中的DispatcherServlet
或者Struts2中的FilterDispatcher
,是Shiro的心臟,所有具體的互動都通過SecurityManager
進行控制,它管理著所有Subject
、且負責進行認證和授權、及會話、快取的管理。 -
ChainResolver:Filter
鏈解析器,用來解析出該次請求需要執行的Filter鏈。 -
PathMatchingFilterChainResolver
:ChainResolver
的實現類,其中還包含了兩個重要元件FilterChainManager
、PatternMatcher
-
FilterChainManager
:管理著Filter和Filter鏈,配合PathMatchingFilterChainResolver
解析出Filter鏈 -
PatternMatcher
:用來進行請求路徑匹配,預設為Ant風格的路徑匹配
先有一個大體的瞭解,那麼對於原始碼分析會有不少幫助。下面會對以上兩個重要的元件進行分析,包括PathMatchingFilterChainResolver
和FilterChainManager
。首先貼一段ShiroFilter
的在配置檔案中的定義:
<!-- Shiro的Web過濾器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="/login" />
<property name="unauthorizedUrl" value="/special/unauthorized" />
<property name="filters">
<util:map>
<entry key="authc" value-ref="formAuthenticationFilter" />
<entry key="logout" value-ref="logoutFilter" />
<entry key="ssl" value-ref="sslFilter"></entry>
</util:map>
</property>
<property name="filterChainDefinitions">
<value>
/resources/** = anon
/plugin/** = anon
/download/** = anon
/special/unauthorized = anon
/register = anon
/login = ssl,authc
/logout = logout
/admin/** = roles[admin]
/** = user
</value>
</property>
</bean>
再來看看PathMatchingFilterChainResolver
和FilterChainManager
的建立過程:
protected FilterChainManager createFilterChainManager() {
// 預設使用的FilterChainManager是DefaultFilterChainManager
DefaultFilterChainManager manager = new DefaultFilterChainManager();
// DefaultFilterChainManager預設會註冊的filters(後面會列出)
Map<String, Filter> defaultFilters = manager.getFilters();
// 將ShiroFilterFactoryBean配置的一些公共屬性(上面配置的loginUrl,successUrl,unauthorizeUrl)應用到預設註冊的filter上去
for (Filter filter : defaultFilters.values()) {
applyGlobalPropertiesIfNecessary(filter);
}
// 處理自定義的filter(上面配置的filters屬性),步驟類似上面
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);
}
// 將Filter新增到manager中去,可以看到對於Filter的管理是依賴於FilterChainManager的
manager.addFilter(name, filter, false);
}
}
// 根據FilterChainDefinition的配置來構建Filter鏈(上面配置的filterChainDefinitions屬性)
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();
// 後面會分析該步的原始碼,功能上就是建立Filter鏈
manager.createChain(url, chainDefinition);
}
}
return manager;
}
下面有必要來看看DefaultFilterChainManager
的原始碼,分析一下上面呼叫到的方法。先來看看他的幾個重要的屬性:
private FilterConfig filterConfig;
private Map<String, Filter> filters; //pool of filters available for creating chains
private Map<String, NamedFilterList> filterChains; //key: chain name, value: chain
其中filterConfig
僅在初始化Filter時有效,而我們自定義的Filter都不是init的,所以該屬性可以暫時忽略()。
而後面兩張map就重要了:filters中快取了所有新增的filter
,filterChains則快取了所有的filterChain
。其中前者的key是filter name,value是Filter
。而後者的key是chain name,value是NamedFilterList
。
有的童鞋可能會問NamedFilterList
是怎麼樣的結構呢,你可以把它當成List<Filter>
,這樣就好理解了吧。下面再分析剛才createFilterChainManager()
中呼叫過的manager
的幾個方法:
- addFilter(快取filter讓manager來管理)
public void addFilter(String name, Filter filter, boolean init) {
addFilter(name, filter, init, true);
}
protected void addFilter(String name, Filter filter, boolean init, boolean overwrite) {
Filter existing = getFilter(name);
if (existing == null || overwrite) {
if (filter instanceof Nameable) {
((Nameable) filter).setName(name);
}
if (init) {
initFilter(filter);
}
this.filters.put(name, filter);
}
}
將filter
快取到filters
這張map
裡,不管是預設註冊的還是自定義的都需要FilterChainManager
來統一管理。
- createChain:建立filterChain並將定義的filter都加進去
// chainName就是攔截路徑"/resources/**",chainDefinition就是多個過濾器名的字串
public void createChain(String chainName, String chainDefinition) {
if (!StringUtils.hasText(chainName)) {
throw new NullPointerException("chainName cannot be null or empty.");
}
if (!StringUtils.hasText(chainDefinition)) {
throw new NullPointerException("chainDefinition cannot be null or empty.");
}
if (log.isDebugEnabled()) {
log.debug("Creating chain [" + chainName + "] from String definition [" + chainDefinition + "]");
}
// 先分離出配置的各個filter,比如
// "authc, roles[admin,user], perms[file:edit]" 分離後的結果是:
// { "authc", "roles[admin,user]", "perms[file:edit]" }
String[] filterTokens = splitChainDefinition(chainDefinition);
// 進一步分離出"[]"內的內容,其中nameConfigPair是一個長度為2的陣列
// 比如 roles[admin,user] 經過解析後的nameConfigPair 為{"roles", "admin,user"}
for (String token : filterTokens) {
String[] nameConfigPair = toNameConfigPair(token);
// 得到了 攔截路徑、filter以及可能的"[]"中的值,那麼執行addToChain
addToChain(chainName, nameConfigPair[0], nameConfigPair[1]);
}
}
- addToChain
public void addToChain(String chainName, String filterName, String chainSpecificFilterConfig) {
if (!StringUtils.hasText(chainName)) {
throw new IllegalArgumentException("chainName cannot be null or empty.");
}
Filter filter = getFilter(filterName);
if (filter == null) {
throw new IllegalArgumentException("There is no filter with name '" + filterName + "' to apply to chain [" + chainName + "] in the pool of available Filters. Ensure a " + "filter with that name/path has first been registered with the addFilter method(s).");
}
// 將"[]"中的匹配關係註冊到filter中
applyChainConfig(chainName, filter, chainSpecificFilterConfig);
// 確保chain已經被加到filterChains這張map中了
NamedFilterList chain = ensureChain(chainName);
// 將該filter加入當前chain
chain.add(filter);
}
至此,FilterChainManager
就建立完了,它無非就是快取了兩張map
,沒有什麼邏輯上的操作。下面將FilterChainManager
設定到PathMatchingFilterChainResolver
中。PathMatchingFilterChainResolver
實現了FilterChainResolver
介面,該介面中只定義了一個方法:
FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain);
通過解析請求來得到一個新的FilterChain
。而PathMatchingFilterChainResolver
實現了該介面,依靠了FilterChainManager
中儲存的chainFilters
和filters
這兩張map來根據請求路徑解析出相應的filterChain
,並且和originalChain
組合起來使用。下面具體看看PathMatchingFilterChainResolver
中的實現:
public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
// 得到 FilterChainManager
FilterChainManager filterChainManager = getFilterChainManager();
if (!filterChainManager.hasChains()) {
return null;
}
String requestURI = getPathWithinApplication(request);
// chainNames就是剛定義的filterChains的keySet,也就是所有的路徑集合(比如:["/resources/**","/login"])
for (String pathPattern : filterChainManager.getChainNames()) {
// 請求路徑是否匹配某個 定義好的路徑:
if (pathMatches(pathPattern, requestURI)) {
if (log.isTraceEnabled()) {
log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "]. " + "Utilizing corresponding filter chain...");
}
// 找到第一個匹配的Filter鏈,那麼就返回一個ProxiedFilterChain
return filterChainManager.proxy(originalChain, pathPattern);
}
}
return null;
}
這裡返回只有兩種情況,要麼是null
,要麼就是一個ProxiedFilterChain
。返回null
並不表示中斷FilterChain
,而是隻用originChain
。而關於ProxiedFilterChain
,它實現了FilterChain
,內部維護了兩份FilterChain
(其實一個是FilterChain
,另一個是List<Filter>
) FilterChain
也就是web.xml
中註冊的Filter
形成的FilterChain
,我們稱之為originChain
。而另一個List<Filter>
則是我們在Shiro中註冊的Filter鏈了,下面看看ProxiedFilterChain
中關於doFilter(...)
的實現:
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (this.filters == null || this.filters.size() == this.index) {
//we've reached the end of the wrapped chain, so invoke the original one:
if (log.isTraceEnabled()) {
log.trace("Invoking original filter chain.");
}
this.orig.doFilter(request, response);
} else {
if (log.isTraceEnabled()) {
log.trace("Invoking wrapped filter at index [" + this.index + "]");
}
this.filters.get(this.index++).doFilter(request, response, this);
}
}
可以看到,它會先執行Shiro中執行的filter
,然後再執行web.xml
中的Filter
。不過要注意的是,需要等到originChain
執行到ShiroFilter
之後才會執行Shiro中的Filter鏈。
至此,兩個元件的建立過程差不多都介紹完了,那麼當這兩個元件建立完畢後,是如何工作的呢?
先從ShiroFilter
入手,因為它是總的攔截器,看看其中的doFilterInternal(...)
方法:
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
throws ServletException, IOException {
Throwable t = null;
try {
final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
final Subject subject = createSubject(request, response);
//noinspection unchecked
subject.execute(new Callable() {
public Object call() throws Exception {
// 其實需要關心的就在這裡
// touch一下session
updateSessionLastAccessTime(request, response);
// 執行Filter鏈
executeChain(request, response, chain);
return null;
}
});
} catch (ExecutionException ex) {
t = ex.getCause();
} catch (Throwable throwable) {
t = throwable;
}
if (t != null) {
if (t instanceof ServletException) {
throw (ServletException) t;
}
if (t instanceof IOException) {
throw (IOException) t;
}
//otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one:
String msg = "Filtered request failed.";
throw new ServletException(msg, t);
}
}
跟進executeChain(...)
方法:
protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain)
throws IOException, ServletException {
FilterChain chain = getExecutionChain(request, response, origChain);
chain.doFilter(request, response);
}
如何得到FilterChain
的呢?如果你認真的看到這裡,那麼你應該不難想到其中肯定利用了剛才註冊的ChainResolver
:
protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
FilterChain chain = origChain;
FilterChainResolver resolver = getFilterChainResolver();
if (resolver == null) {
log.debug("No FilterChainResolver configured. Returning original FilterChain.");
return origChain;
}
FilterChain resolved = resolver.getChain(request, response, origChain);
if (resolved != null) {
log.trace("Resolved a configured FilterChain for the current request.");
chain = resolved;
} else {
log.trace("No FilterChain configured for the current request. Using the default.");
}
return chain;
}
猜對了~並且也驗證了當resolver.getChain(...)
返回null
時,直接使用originChain
了。然後執行返回的FilterChain
的doFilter(...)
方法。這個過程我們再脫離程式碼來分析一下:當我們從瀏覽器發出一個請求,究竟發生了什麼?
這裡只站在Filter
的層面來分析。伺服器啟動後,讀取web.xml
中的filter
、filter-mapping
節點後組成FilterChain
,對請求進行攔截。攔截的順序按照filter節點的定義順序,Shiro利用ShiroFilter
來充當一個總的攔截器來分發所有需要被Shiro攔截的請求,所以我們看到在Shiro中我們還可以自定義攔截器。ShiroFilter
根據它在攔截器中的位置,只要執行到了那麼就會暫時中斷原FilterChain
的執行,先執行Shiro中定義的Filter
,最後再執行原FilterChian
。可以打個比方,比如說本來有一條鐵鏈,一直螞蟻從鐵鏈的開端往末端爬,其中某一環叫ShiroFilter
,那麼當螞蟻爬到ShiroFilter
這一環時,將鐵鏈打斷,並且接上另一端鐵鏈(Shiro中自定義的Filter
),這樣就構成了一條新的鐵鏈。然後螞蟻繼續爬行(後續的執行過程)。
最後附上預設註冊的filters:
public enum DefaultFilter {
anon(AnonymousFilter.class),
authc(FormAuthenticationFilter.class),
authcBasic(BasicHttpAuthenticationFilter.class),
logout(LogoutFilter.class),
noSessionCreation(NoSessionCreationFilter.class),
perms(PermissionsAuthorizationFilter.class),
port(PortFilter.class),
rest(HttpMethodPermissionFilter.class),
roles(RolesAuthorizationFilter.class),
ssl(SslFilter.class),
user(UserFilter.class);
}
水平有限,寫得蠻不容易,看原始碼加寫花了整整2天。希望對大家能有幫助~