Shiro許可權管理框架(五):自定義Filter實現及其問題排查記錄
明確需求
在使用Shiro
的時候,鑑權失敗一般都是返回一個錯誤頁或者登入頁給前端,特別是後臺系統,這種模式用的特別多。但是現在的專案越來越多的趨向於使用前後端分離的方式開發,這時候就需要響應Json
資料給前端了,前端再根據狀態碼做相應的操作。那麼Shiro框架能不能在鑑權失敗的時候直接返回Json
資料呢?答案當然是可以。
其實Shiro
的自定義過濾器功能特別強大,可以實現很多實用的功能,向前端返回Json
資料自然不在話下。通常我們沒有去關注它是因為Shiro
內建的一下過濾器功能已經比較全了,後臺系統的許可權控制基本上只需要使用Shiro
內建的一些過濾器就能實現了,此處再次貼上這個圖。
相關文件地址:http://shiro.apache.org/web.html#default-filters
我最近的一個專案是需要為手機APP提供功能介面,需要做使用者登入,Session
持久化以及Session
共享,但不需要細粒度的許可權控制。面對這個需求我第一個想到的就是整合Shiro
了,Session
的持久化及共享在Shiro
系列第二篇已經講過了,那麼這篇順便用一下Shiro
中的自定義過濾器。因為不需要提供細粒度許可權控制,只需要做登入鑑權,而且鑑權失敗後需要向前端響應Json
資料,那麼使用自定義Filter
再好不過了。
自定義Filter
還是以第一篇的Demo為例,專案地址在文章尾部有放上,本篇在之前的程式碼上繼續新增功能。
首發地址:https://www.guitu18.com/post/2020/01/06/64.html
在實現自定義Filter之前,我們先看看這個類:org.apache.shiro.web.filter.AccessControlFilter
,點開它的子類,發現子類全部都是org.apache.shiro.web.filter.authc
和org.apache.shiro.web.filter.authz
這兩個包下的,大多都繼承了AccessControlFilter
這個類。這些子類的類名是不是很眼熟,看上面那張我貼了三遍的圖,大部分都在這裡面呢。
看來AccessControlFilter
這個類是跟Shiro許可權過濾密切相關的,那麼先看看它的體系結構:
它的頂級父類是javax.servlet.Filter
Filter
來實現的。自定義Filter
同樣需要實現AccessControlFilter
,這裡我們新增一個登入驗證過濾器,程式碼如下:
public class AuthLoginFilter extends AccessControlFilter {
// 未登入登陸返狀態回碼
private int code;
// 未登入登陸返提示資訊
private String message;
public AuthLoginFilter(int code, String message) {
this.code = code;
this.message = message;
}
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse,
Object mappedValue) throws Exception {
Subject subject = SecurityUtils.getSubject();
// 這裡配合APP需求我只需要做登入檢測即可
if (subject != null && subject.isAuthenticated()) {
// TODO 登入檢測通過,這裡可以新增一些自定義操作
return Boolean.TRUE;
}
// 登入檢測失敗返貨False後會進入下面的onAccessDenied()方法
return Boolean.FALSE;
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest,
ServletResponse servletResponse) throws Exception {
PrintWriter out = null;
try {
// 這裡就很簡單了,向Response中寫入Json響應資料,需要宣告ContentType及編碼格式
servletResponse.setCharacterEncoding("UTF-8");
servletResponse.setContentType("application/json; charset=utf-8");
out = servletResponse.getWriter();
out.write(JSONObject.toJSONString(R.error(code, message)));
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null) {
out.close();
}
}
return Boolean.FALSE;
}
}
自定義過濾器寫好了,現在需要把它交給Shiro管理:
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 新增登入過濾器
Map<String, Filter> filters = new LinkedHashMap<>();
// 這裡註釋的一行是我這次踩的一個小坑,我一開始按下面這麼配置產生一個我意料之外的問題
// filters.put("authLogin", authLoginFilter());
// 正確的配置是需要我們自己new出來,不能將這個Filter交給Spring管理,後面會說明
filters.put("authLogin", new AuthLoginFilter(500, "未登入或登入超時"));
shiroFilterFactoryBean.setFilters(filters);
// 設定過濾規則
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/api/login", "anon");
filterMap.put("/api/**", "authLogin");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
如此Shiro新增自定義過濾器就完成了。自定義的Filter
可以新增多個以實現不同的需求,你僅僅需要在filters
中將過濾器起好名字put
進去,並在filterChainMap
中新增過濾器別名和路徑的對映就可以使用這個過濾器了。需要注意的一點就是過濾器是從前往後順序匹配的,所以要把範圍大的路徑放在後面put
進去。
到這裡自定義Filter功能已經實現了,後面是採坑排查記錄,不感興趣可以跳過。
問題排查
前半段介紹瞭如何使用Shiro
的自定義Filter
功能實現過濾,在Shiro
配置程式碼中我提了一句這次配置踩的一個小坑,如果我們將自定義的Filter交給Spring管理,會產生一些意料之外的問題。確實,通常在Spring專案中做配置時,我們都預設將Bean交由Spring管理,一般不會有什麼問題,但是這次不一樣,先看程式碼如下:
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
...
filters.put("authLogin", authLoginFilter());
...
filterMap.put("/api/login", "anon");
filterMap.put("/api/**", "authLogin");
...
}
@Bean
public AuthLoginFilter authLoginFilter() {
return new AuthLoginFilter(500, "未登入或登入超時");
}
這樣配置後造成的現象是:無論前面的過濾器是否放行,最終都會走到自定義的AuthLoginFilter
過濾器。
比如上面的配置,我們訪問/api/login
正常來講會被anon
匹配到AnonymousFilter
中,這裡是什麼都沒做直接放行的,但是放行後還會繼續走到AuthLoginFilter
中,怎麼會這樣,說好的按順序匹配呢,怎麼不按套路出牌。
打斷點一路往上追溯,我們找到了ApplicationFilterChain
這裡,它是Tomcat
所實現的一個Java Servlet API
的規範。所有的請求都必須通過filters
裡的過濾器層層過濾後才會呼叫Servlet
中的方法service()
方法。這裡包括Spring中的各種過濾器,全部都是註冊到這裡來的。
前面的四個Filter都是Spring的,第五個是Shiro
的ShiroFilterFactoryBean
,它的內部也維護了一個filters
,用來儲存Shiro
內建的一些過濾器和我們自定義的過濾器,Tomcat
所維護的filters
和Shiro
維護的filters
是一個父子層級的關係,Shiro
中的ShiroFilterFactoryBean
僅僅只是Tomcat
裡filters
中的一員。點開看ShiroFilterFactoryBean
檢視,果然Shiro
內建的一些過濾器全都按順序排著呢,我們自定義的AuthLoginFilter
在最後一個。
但是,再看看Tomcat
中的第六個過濾器,居然也是我們自定義的AuthLoginFilter
,它同時出現在Tomcat
和Shiro
的filters
中,這樣也就造成了前面提到的問題,Shiro
在匹配到anon
之後確實會將請求放行,但是在外層Tomcat
的Filter
中依舊被匹配上了,造成的現象好像是Shiro
的Filter
配置規則失效了,其實這個問題跟Shiro
並沒有關係。
問題的根源找到了,想要解決這個問題必須找到這個自定義的Filter
何時被新增到Tomcat
的過濾器執行鏈中以及其原因。
追根溯源
關於這個問題我找到了ServletContextInitializerBeans
這個類中,它在Spring啟動時就會初始化,在它的構造方法中做了很多初始化相關的操作。至於這一系列初始化流程就不得不提ServletContextInitializer
相關知識點了,關於它的內容完全可以另開一片部落格細說了。先看看ServletContextInitializerBeans
的構造方法:
@SafeVarargs
public ServletContextInitializerBeans(ListableBeanFactory beanFactory,
Class<? extends ServletContextInitializer>... initializerTypes) {
this.initializers = new LinkedMultiValueMap<>();
this.initializerTypes = (initializerTypes.length != 0) ? Arrays.asList(initializerTypes)
: Collections.singletonList(ServletContextInitializer.class);
// 上面提到的Filter正是在這個方法開始一步步被新增到ApplicationFilterChain中的
addServletContextInitializerBeans(beanFactory);
addAdaptableBeans(beanFactory);
List<ServletContextInitializer> sortedInitializers = this.initializers.values().stream()
.flatMap((value) -> value.stream().sorted(AnnotationAwareOrderComparator.INSTANCE))
.collect(Collectors.toList());
this.sortedList = Collections.unmodifiableList(sortedInitializers);
logMappings(this.initializers);
}
上面提到的ApplicationFilterChain
中的Filter
正是在addServletContextInitializerBeans(beanFactory)
這個方法開始一步步被新增到Filters
中的,限於篇幅這裡就看一下關鍵步驟。
private void addServletContextInitializerBeans(ListableBeanFactory beanFactory) {
for (Class<? extends ServletContextInitializer> initializerType : this.initializerTypes) {
for (Entry<String, ? extends ServletContextInitializer> initializerBean :
// 這裡根據type獲取Bean列表並遍歷
getOrderedBeansOfType(beanFactory, initializerType)) {
// 此處開始新增對應的ServletContextInitializer
addServletContextInitializerBean(initializerBean.getKey(), initializerBean.getValue(), beanFactory);
}
}
}
addServletContextInitializerBeans(beanFactory)
一路走下去會到達getOrderedBeansOfType()
方法中,然後呼叫了beanFactory
的getBeanNamesForType()
,預設的實現在DefaultListableBeanFactory
中,這裡所貼前後刪減掉了無關程式碼:
private String[] doGetBeanNamesForType(ResolvableType type, boolean includeNonSingletons, boolean allowEagerInit) {
List<String> result = new ArrayList<>();
// 檢查所有的Bean
for (String beanName : this.beanDefinitionNames) {
// 當這個Bean名稱沒有定義為其他bean的別名時,才進行匹配
if (!isAlias(beanName)) {
RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
// 檢查Bean的完整性,檢測是否是抽象類,是否懶載入等等屬性
if (!mbd.isAbstract() && (allowEagerInit || (mbd.hasBeanClass() || !mbd.isLazyInit() ||
isAllowEagerClassLoading()) && !requiresEagerInitForType(mbd.getFactoryBeanName()))) {
// 匹配的Bean是否是FactoryBean,對於FactoryBean,需要匹配它建立的物件
boolean isFactoryBean = isFactoryBean(beanName, mbd);
BeanDefinitionHolder dbd = mbd.getDecoratedDefinition();
// 這裡也是做完整性檢查
boolean matchFound = (allowEagerInit || !isFactoryBean || (dbd != null && !mbd.isLazyInit())
|| containsSingleton(beanName)) && (includeNonSingletons ||
(dbd != null ? mbd.isSingleton() : isSingleton(beanName))) && isTypeMatch(beanName, type);
if (!matchFound && isFactoryBean) {
// 對於FactoryBean,接下來嘗試匹配FactoryBean例項本身
beanName = FACTORY_BEAN_PREFIX + beanName;
matchFound = (includeNonSingletons || mbd.isSingleton()) && isTypeMatch(beanName, type);
}
if (matchFound) {
result.add(beanName);
}
}
}
}
return StringUtils.toStringArray(result);
}
到這裡就是關鍵所在了,它會根據目標型別呼叫isTypeMatch(beanName, type)
匹配每一個被Spring接管的Bean
,isTypeMatch
方法很長,這裡就不貼了,有興趣的可以自行去看看,它位於AbstractBeanFactory
中。這裡匹配的type
就是ServletContextInitializerBeans
遍歷自構造方法中的initializerTypes
列表。
從doGetBeanNamesForType
出來後,再看這個方法:
private void addServletContextInitializerBean(String beanName,
ServletContextInitializer initializer, ListableBeanFactory beanFactory) {
if (initializer instanceof ServletRegistrationBean) {
Servlet source = ((ServletRegistrationBean<?>) initializer).getServlet();
addServletContextInitializerBean(Servlet.class, beanName, initializer,
beanFactory, source);
}
else if (initializer instanceof FilterRegistrationBean) {
Filter source = ((FilterRegistrationBean<?>) initializer).getFilter();
addServletContextInitializerBean(Filter.class, beanName, initializer,
beanFactory, source);
}
else if (initializer instanceof DelegatingFilterProxyRegistrationBean) {
String source = ((DelegatingFilterProxyRegistrationBean) initializer)
.getTargetBeanName();
addServletContextInitializerBean(Filter.class, beanName, initializer,
beanFactory, source);
}
else if (initializer instanceof ServletListenerRegistrationBean) {
EventListener source = ((ServletListenerRegistrationBean<?>) initializer)
.getListener();
addServletContextInitializerBean(EventListener.class, beanName, initializer,
beanFactory, source);
}
else {
addServletContextInitializerBean(ServletContextInitializer.class, beanName,
initializer, beanFactory, initializer);
}
}
前面兩個配置過Filter
和Servlet
的應該很熟悉,Spring
中新增自定義Filter
經常這麼用,新增Servlet
同理:
@Bean
public FilterRegistrationBean xssFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setFilter(new XxxFilter());
registration.addUrlPatterns("/*");
registration.setName("xxxFilter");
return registration;
}
這樣Spring
就會將其新增到過濾器執行鏈中,當然這只是新增Filter
的眾多方式之一。
解決方案
那麼問題的根源找到了,被Spring
接管的Bean
中所有的Filter
都會被新增到ApplicationFilterChain
,那我不讓Spring
接管我的AuthLoginFilter
不就行了。如何做?配置的時候直接new
出來,還記得前面的那兩行程式碼嗎:
// 這裡註釋的一行是我這次踩的一個小坑,我一開始按下面這麼配置產生了一個我意料之外的問題
// filters.put("authLogin", authLoginFilter());
// 正確的配置是需要我們自己new出來,不能將這個Filter交給Spring管理
filters.put("authLogin", new AuthLoginFilter(500, "未登入或登入超時"));
OK,問題解決,就是這麼簡單。但就是這麼小小的一個問題,在不清楚問題產生的原因的情況下,根本想不到是Spring
接管Filter
造成的,瞭解了底層,才能更好的排查問題。
尾巴
Shiro
中自定義Filter
僅需要繼承AccessControlFilter
類後實現參與過濾的兩個方法,再將其配置到ShiroFilterFactoryBean
中即可。- 需要注意的點是,因為
Spring
的初始化機制,我們自定義的Filter
如果被Spring
接管,那麼會被Spring
新增到ApplicationFilterChain
中,導致這個自定義過濾器會被重複執行,也就是無論Shiro
中的過濾器過濾結果如何,最後依舊會走到被新增到ApplicationFilterChain
中的自定義過濾器。 - 解決這個問題的方法非常簡單,不讓
Spring
接管我們的Filter
,直接new
出來配置到Shiro
中即可。 - 碼海無涯,不進則退,日積跬步,以至千里。
Shiro系列部落格專案原始碼地址:
Gitee:https://gitee.com/guitu18/ShiroDemo
GitHub:https://github.com/guitu18/ShiroDemo
- Shiro 許可權管理框架(一):Shiro 的基本使用
- Shiro 許可權管理框架(二):Shiro 結合 Redis 實現分散式或叢集環境下的 Session 共享
- Shiro 許可權管理框架(三):Shiro 中許可權過濾器的初始化流程和實現原理
- Shiro 許可權管理框架(四):深入分析 Shiro 中的 Session 管理
- Shiro 許可權管理框架(五):自定義 Filter 實現及其問題排查記錄