[轉(zhuǎn)](http://www.hillfly.com/2017/179.html)
最近忙著研究在 Springboot 上使用 Shiro 的問題滑进。剛好就遇到個(gè)詭異事驯镊,百度 Google 也沒找到啥有價(jià)值的信息够庙,幾番周折自己解決了,這里稍微記錄下践盼。
自定義 Filter
Shiro 支持自定義 Filter 大家都知道思瘟,也經(jīng)常用环础,這里我也用到了一個(gè)自定義 Filter,主要用于驗(yàn)證接口調(diào)用的 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("操作授權(quán)失斚叩谩!" + 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 是從數(shù)據(jù)庫讀取的贯钩,如下:
我們想要達(dá)到的效果是,除了登陸和訪問 Druid 監(jiān)控頁面外办素,訪問其它地址一律要先驗(yàn)證 Token角雷,即走我們的自定義過濾器。
修改完畢后啟動(dòng)無異常性穿,我們訪問地址驗(yàn)證下勺三。
POST /api/login
{
"hasError": true,
"errors": {
"httpStatus": 401,
"errorCode": "4001",
"errorMsg": "授權(quán)異常:操作授權(quán)失敗需曾!AccessToken失效吗坚!",
"timestamp": "2017-06-10 11:08:03"
}
}
funny,結(jié)果出乎意料呆万,居然登陸接口走了咱們的那個(gè)自定義 Filter商源??黑人問號(hào)臉桑嘶。炊汹。。
問題排查
FilterChain
首先檢查 Shiro FilterChain 加載的順序是否異常逃顶。
1讨便、集合容器使用 LinkedHashMap,保證的 FilterChain 的順序以政。
2霸褒、從數(shù)據(jù)庫讀取 Filter 時(shí)也是按 sort 排序的。
從調(diào)試結(jié)果來看盈蛮,加載順序和數(shù)據(jù)并沒有任何問題废菱,都是正確的。
排除了自身的數(shù)據(jù)問題抖誉,那就要往深處挖掘原因了殊轴,有了之前解決 Quartz 問題的經(jīng)歷,這次毫不猶豫就決定跟源碼跟蹤 Filter 注冊到匹配的過程袒炉。
Filter 注冊
要查明白為何匹配異常旁理,就要先弄清楚咱們的自定義 Filter 是如何注冊到 Shiro 的,顯然我磁,問題的關(guān)鍵在于 ShiroFilter 返回的 ShiroFilterFactoryBean 這個(gè)類中孽文,我們打開看看驻襟。很快,我們就鎖定了關(guān)鍵 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 中的默認(rèn) Filter芋哭;
- 加載自定義 Filter沉衣;
- 加載 FFilterChainDefinitionMap;
弄清楚了這 Filter 的加載與注冊减牺,那這與我們要解決的問題有何關(guān)系呢豌习?首先我們懷疑這里獲取的 Filter 是異常的,調(diào)試打個(gè)斷點(diǎn)看看拔疚。
然而奇怪的是斑鸦,從調(diào)試結(jié)果來看,一切加載的 Filter 都如我們預(yù)想的那樣草雕,并無異常。
Filter Match
既然基本排除了 Filter 加載上出現(xiàn)問題的可能固以,那么就要來排查 Filter 匹配的問題了墩虹。
重點(diǎn)在于 AbstractShiroFilter 的 doFilterInternal(),這里是匹配的起點(diǎn)憨琳。
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);
}
跟蹤到最后诫钓,會(huì)進(jìn)入到一個(gè)關(guān)鍵方法:
//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);
}
}
顯然,這里就是進(jìn)行 URL 匹配的地方篙螟。難道是這里匹配出異常了菌湃?我們打個(gè)斷點(diǎn)在這里再訪問一下。然而怪異出現(xiàn)了遍略,沒有進(jìn)斷點(diǎn)惧所,直接返回了異常信息,根本沒有進(jìn)行匹配P餍印下愈!我們再對自定義 filter 斷點(diǎn)調(diào)試后發(fā)現(xiàn)了 Filter 調(diào)用鏈如下:
MMP 的,完全沒有不是按我們預(yù)想的那樣進(jìn)行調(diào)用蕾久。這 TM 居然是作為 Spring 的全局 Filter 被調(diào)用了势似。Shiro 的 Filter 優(yōu)先級居然失效了?我們都知道之前在 SpringMVC+Shiro 時(shí)僧著,都會(huì)把 Shiro 的 Filter 配置順序盡量放前履因,以達(dá)到優(yōu)先加載的目的。難道這里沒有走 Shiro 的匹配是因?yàn)檫@個(gè)嗎盹愚?栅迄?難道是因?yàn)?Springboot 先加載了我們自定義的 Filter,然后再加載了 ShiroFilter 嗎杯拐,然后這個(gè) Filter 優(yōu)先順序就出問題了霞篡?
我們將斷點(diǎn)打到 ApplicationFilterChain.java 的 internalDoFilter() 中進(jìn)行驗(yàn)證下:
J勒帷!果然袄时污淋!咱們的自定義 Filter 居然還在 ShiroFilter 之前,這就導(dǎo)致請求被我們自定義 Filter 先消費(fèi)掉了余掖。寸爆。ShiroFilter 成了擺設(shè)。
那么把咱們的 Bean 放到 ShiroFilter 后面會(huì)如何呢盐欺?
@Bean
public ShiroFilterFactoryBean shiroFilter(){}
@Bean
public AccessTokenFilter accessTokenFilter(){}
果然順序變了赁豆,那么問題解決了嗎?
——沒有冗美,問題依舊魔种,咱們的 Filter 還是跑了,返回了異常粉洼。
看來應(yīng)該不是這里的順序問題节预,我們回過頭來繼續(xù)看 ApplicationFilterChain.java 的 internalDoFilter(),系統(tǒng)會(huì)將注冊的 filters 逐一調(diào)用属韧,也就是說無論我們的順序如何安拟,F(xiàn)ilter 最終都是會(huì)被調(diào)用的。
問題解決
眼下我暫時(shí)有兩種辦法去解決這個(gè)問題:
- 修改 AccessTokenFilter宵喂,在 Filter 內(nèi)部加入 path match 方法對需要驗(yàn)證 token 的路徑進(jìn)行過濾糠赦。
- 將咱們的自定義 Filter 注冊到 Shiro,不注冊到 ApplicationFilterChain锅棕。
顯然方案一是不可取的拙泽,這樣修改范圍過大,得不償失了裸燎。那我們怎么去實(shí)現(xiàn)第二個(gè)方法呢奔滑?SpringBoot 提供了 FilterRegistrationBean 方便我們對 Filter 進(jìn)行管理。
@Bean
public FilterRegistrationBean registration(AccessTokenFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
將不需要注冊的 Filter 注入方法即可顺少。這時(shí)候再啟動(dòng)項(xiàng)目進(jìn)行測試朋其,就可以發(fā)現(xiàn) filters 已經(jīng)不存在咱們的自定義 Filter 了。
還有個(gè)辦法不需要使用到 FilterRegistrationBean脆炎,因?yàn)槲覀儗?AccessTokenFilter 注冊為了 Bean 交給 Spring 托管了梅猿,所以它會(huì)被自動(dòng)注冊到 FilterChain 中,那我們?nèi)绻话阉詾?Bean 就可以避免這個(gè)問題了秒裕。
/**
* 不需要顯示注冊Bean了
@Bean
public AccessTokenFilter accessTokenFilter(){}
**/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager,
IUrlFilterService urlFilterService) {
//省略
filterMap.put("hasToken", new AccessTokenFilter());
//省略
}