明確需求 在使用 的時候,鑒權失敗一般都是返回一個錯誤頁或者登錄頁給前端,特別是後臺系統,這種模式用的特別多。但是現在的項目越來越多的趨向於使用前後端分離的方式開發,這時候就需要響應 數據給前端了,前端再根據狀態碼做相應的操作。那麼Shiro框架能不能在鑒權失敗的時候直接返回 數據呢?答案當然是可以 ...
明確需求
在使用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
,前面我們也說過,Shiro中所有的許可權過濾都是基於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