springboot整合shiro遭遇自定義filter異常(自定義FormAuthenticationFilter)
最近忙著研究在 Springboot 上使用 Shiro 的問題。剛好就遇到個詭異事,百度 Google 也沒找到啥有價值的資訊,幾番周折自己解決了,這裡稍微記錄下。
自定義 Filter
Shiro 支援自定義 Filter 大家都知道,也經常用,這裡我也用到了一個自定義 Filter,主要用於驗證介面呼叫的 AccessToken 是否有效。
// AccessTokenFilter.java public class AccessTokenFilter extends AccessControlFilter { @Override protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) { if (isValidAccessToken(request)) { return true; } return false; } @Override protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { throw new UnAuthorizedException("操作授權失敗!" + SysConstant.ACCESSTOKEN + "失效!"); } }
// ShiroConfiguration.java @Bean public AccessTokenFilter accessTokenFilter(){ return new AccessTokenFilter(); } @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager, IUrlFilterService urlFilterService) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); // 自定義過濾器 Map<String, Filter> filterMap = shiroFilterFactoryBean.getFilters(); filterMap.put("hasToken", accessTokenFilter()); shiroFilterFactoryBean.setFilters(filterMap); // URL過濾 Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); List<UrlFilter> urlFilterList = urlFilterService.selectAll(); for (UrlFilter filter : urlFilterList) { filterChainDefinitionMap.put(filter.getFilterUrl(), filter.getFilterList()); } shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; }
ShiroFilter 中的 FilterChain 是從資料庫讀取的,如下:
id | url | filter | sort |
---|---|---|---|
1 | /druid/** | anon | 1 |
2 | /api/login | anon | 2 |
3 | /** | hasToken,authc | 3 |
我們想要達到的效果是,除了登陸和訪問 Druid 監控頁面外,訪問其它地址一律要先驗證 Token,即走我們的自定義過濾器。
修改完畢後啟動無異常,我們訪問地址驗證下。
- POST /api/login
{
"hasError": true,
"errors": {
"httpStatus": 401,
"errorCode": "4001",
"errorMsg": "授權異常:操作授權失敗!AccessToken失效!",
"timestamp": "2017-06-10 11:08:03"
}
}
funny,結果出乎意料,居然登陸介面走了咱們的那個自定義 Filter??黑人問號臉。。。
問題排查
FilterChain
首先檢查 Shiro FilterChain 載入的順序是否異常。
1、集合容器使用 LinkedHashMap,保證的 FilterChain 的順序。
2、從資料庫讀取 Filter 時也是按 sort 排序的。
從除錯結果來看,載入順序和資料並沒有任何問題,都是正確的。
排除了自身的資料問題,那就要往深處挖掘原因了,有了之前解決 Quartz 問題的經歷,這次毫不猶豫就決定跟原始碼跟蹤 Filter 註冊到匹配的過程。
Filter 註冊
要查明白為何匹配異常,就要先弄清楚咱們的自定義 Filter 是如何註冊到 Shiro 的,顯然,問題的關鍵在於 ShiroFilter 返回的 ShiroFilterFactoryBean 這個類中,我們開啟看看。很快,我們就鎖定了關鍵 method:
//ShiroFilterFactoryBean.java
protected AbstractShiroFilter createInstance() throws Exception {
log.debug("Creating Shiro Filter instance.");
SecurityManager securityManager = this.getSecurityManager();
String msg;
if(securityManager == null) {
msg = "SecurityManager property must be set.";
throw new BeanInitializationException(msg);
} else if(!(securityManager instanceof WebSecurityManager)) {
msg = "The security manager does not implement the WebSecurityManager interface.";
throw new BeanInitializationException(msg);
} else {
FilterChainManager manager = this.createFilterChainManager();
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
chainResolver.setFilterChainManager(manager);
return new ShiroFilterFactoryBean.SpringShiroFilter((WebSecurityManager)securityManager, chainResolver);
}
}
protected FilterChainManager createFilterChainManager() {
DefaultFilterChainManager manager = new DefaultFilterChainManager();
Map<String, Filter> defaultFilters = manager.getFilters();
Iterator var3 = defaultFilters.values().iterator();
while(var3.hasNext()) {
Filter filter = (Filter)var3.next();
this.applyGlobalPropertiesIfNecessary(filter);
}
Map<String, Filter> filters = this.getFilters();
String name;
Filter filter;
if(!CollectionUtils.isEmpty(filters)) {
for(Iterator var10 = filters.entrySet().iterator(); var10.hasNext(); manager.addFilter(name, filter, false)) {
Entry<String, Filter> entry = (Entry)var10.next();
name = (String)entry.getKey();
filter = (Filter)entry.getValue();
this.applyGlobalPropertiesIfNecessary(filter);
if(filter instanceof Nameable) {
((Nameable)filter).setName(name);
}
}
}
Map<String, String> chains = this.getFilterChainDefinitionMap();
if(!CollectionUtils.isEmpty(chains)) {
Iterator var12 = chains.entrySet().iterator();
while(var12.hasNext()) {
Entry<String, String> entry = (Entry)var12.next();
String url = (String)entry.getKey();
String chainDefinition = (String)entry.getValue();
manager.createChain(url, chainDefinition);
}
}
return manager;
}
//DefaultFilterChainManager.java
public DefaultFilterChainManager() {
this.addDefaultFilters(false);
}
//DefaultFilter.java
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);
}
看到這總算弄清楚 Shiro 載入 Filter 的順序:
- 載入 DefaultFilter 中的預設 Filter;
- 載入自定義 Filter;
- 載入 FFilterChainDefinitionMap;
弄清楚了這 Filter 的載入與註冊,那這與我們要解決的問題有何關係呢?首先我們懷疑這裡獲取的 Filter 是異常的,除錯打個斷點看看。
然而奇怪的是,從除錯結果來看,一切載入的 Filter 都如我們預想的那樣,並無異常。
Filter Match
既然基本排除了 Filter 載入上出現問題的可能,那麼就要來排查 Filter 匹配的問題了。
重點在於 AbstractShiroFilter 的 doFilterInternal(),這裡是匹配的起點。
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain) throws ServletException, IOException {
Throwable t = null;
try {
final ServletRequest request = this.prepareServletRequest(servletRequest, servletResponse, chain);
final ServletResponse response = this.prepareServletResponse(request, servletResponse, chain);
Subject subject = this.createSubject(request, response);
subject.execute(new Callable() {
public Object call() throws Exception {
AbstractShiroFilter.this.updateSessionLastAccessTime(request, response);
AbstractShiroFilter.this.executeChain(request, response, chain);
return null;
}
});
} catch (ExecutionException var8) {
t = var8.getCause();
} catch (Throwable var9) {
t = var9;
}
if(t != null) {
if(t instanceof ServletException) {
throw (ServletException)t;
} else if(t instanceof IOException) {
throw (IOException)t;
} else {
String msg = "Filtered request failed.";
throw new ServletException(msg, t);
}
}
}
protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain) throws IOException, ServletException {
FilterChain chain = this.getExecutionChain(request, response, origChain);
chain.doFilter(request, response);
}
跟蹤到最後,會進入到一個關鍵方法:
//PathMatchingFilterChainResolver.java
public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
FilterChainManager filterChainManager = this.getFilterChainManager();
if(!filterChainManager.hasChains()) {
return null;
} else {
String requestURI = this.getPathWithinApplication(request);
Iterator var6 = filterChainManager.getChainNames().iterator();
String pathPattern;
do {
if(!var6.hasNext()) {
return null;
}
pathPattern = (String)var6.next();
} while(!this.pathMatches(pathPattern, requestURI));
if(log.isTraceEnabled()) {
log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "]. Utilizing corresponding filter chain...");
}
return filterChainManager.proxy(originalChain, pathPattern);
}
}
顯然,這裡就是進行 URL 匹配的地方。難道是這裡匹配出異常了?我們打個斷點在這裡再訪問一下。然而怪異出現了,沒有進斷點,直接返回了異常資訊,根本沒有進行匹配!!我們再對自定義 filter 斷點除錯後發現了 Filter 呼叫鏈如下:
MMP 的,完全沒有不是按我們預想的那樣進行呼叫。這 TM 居然是作為 Spring 的全域性 Filter 被呼叫了。Shiro 的 Filter 優先順序居然失效了?我們都知道之前在 SpringMVC+Shiro 時,都會把 Shiro 的 Filter 配置順序儘量放前,以達到優先載入的目的。難道這裡沒有走 Shiro 的匹配是因為這個嗎??難道是因為 Springboot 先載入了我們自定義的 Filter,然後再載入了 ShiroFilter 嗎,然後這個 Filter 優先順序就出問題了?
我們將斷點打到 ApplicationFilterChain.java 的 internalDoFilter() 中進行驗證下:
!!果然啊!咱們的自定義 Filter 居然還在 ShiroFilter 之前,這就導致請求被我們自定義 Filter 先消費掉了。。ShiroFilter 成了擺設。
那麼把咱們的 Bean 放到 ShiroFilter 後面會如何呢?
@Bean
public ShiroFilterFactoryBean shiroFilter(){}
@Bean
public AccessTokenFilter accessTokenFilter(){}
果然順序變了,那麼問題解決了嗎?
——沒有,問題依舊,咱們的 Filter 還是跑了,返回了異常。
看來應該不是這裡的順序問題,我們回過頭來繼續看 ApplicationFilterChain.java 的 internalDoFilter(),系統會將註冊的 filters 逐一呼叫,也就是說無論我們的順序如何,Filter 最終都是會被呼叫的。
問題解決
眼下我暫時有兩種辦法去解決這個問題:
- 修改 AccessTokenFilter,在 Filter 內部加入 path match 方法對需要驗證 token 的路徑進行過濾。
- 將咱們的自定義 Filter 註冊到 Shiro,不註冊到 ApplicationFilterChain。
顯然方案一是不可取的,這樣修改範圍過大,得不償失了。那我們怎麼去實現第二個方法呢?SpringBoot 提供了 FilterRegistrationBean 方便我們對 Filter 進行管理。
@Bean
public FilterRegistrationBean registration(AccessTokenFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
將不需要註冊的 Filter 注入方法即可。這時候再啟動專案進行測試,就可以發現 filters 已經不存在咱們的自定義 Filter 了。
還有個辦法不需要使用到 FilterRegistrationBean,因為我們將 AccessTokenFilter 註冊為了 Bean 交給 Spring 託管了,所以它會被自動註冊到 FilterChain 中,那我們如果不把它註冊為 Bean 就可以避免這個問題了。
/**
* 不需要顯示註冊Bean了
@Bean
public AccessTokenFilter accessTokenFilter(){}
**/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager,
IUrlFilterService urlFilterService) {
//省略
filterMap.put("hasToken", new AccessTokenFilter());
//省略
}