前言
在使用 Shiro 的過程中,遇到一個痛點(diǎn),就是對 restful 支持不太好,也查了很多資料,各種各樣的方法都有,要不就是功能不完整,要不就是解釋不清楚,還有一些對原有功能的侵入性太強(qiáng),經(jīng)過一番探索,算是最簡的配置下完成了需要的功能,這里給大家分享下。大家如果又更好的方案,也可以在評論區(qū)留言,互相探討下。
雖然深入到了源碼進(jìn)行分析,但過程并不復(fù)雜,希望大家可以跟著我的思路捋順了耐心看下去,而不是看見源碼貼就抵觸。
分析
首先先回顧下 Shiro 的過濾器鏈,一般我們都有如下配置:
/login.html = anon
/login = anon
/users = perms[user:list]
/** = authc
不太熟悉的朋友可以了解下這篇文章:Shiro 過濾器。
其中 /users 請求對應(yīng)到 perms 過濾器,對應(yīng)的類: org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter,其中的 onAccessDenied 方法是在沒有權(quán)限時被調(diào)用的, 源碼如下:
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
Subject subject = getSubject(request, response);
// 如果未登錄, 則重定向到配置的 loginUrl
if (subject.getPrincipal() == null) {
saveRequestAndRedirectToLogin(request, response);
} else {
// 如果當(dāng)前用戶沒有權(quán)限, 則跳轉(zhuǎn)到 UnauthorizedUrl
// 如果沒有配置 UnauthorizedUrl, 則返回 401 狀態(tài)碼.
String unauthorizedUrl = getUnauthorizedUrl();
if (StringUtils.hasText(unauthorizedUrl)) {
WebUtils.issueRedirect(request, response, unauthorizedUrl);
} else {
WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
return false;
}
我們可以在這里可以判斷當(dāng)前請求是否時 AJAX 請求,如果是,則不跳轉(zhuǎn)到 logoUrl 或 UnauthorizedUrl 頁面,而是返回 JSON 數(shù)據(jù)。
還有一個方法是 pathsMatch,是將當(dāng)前請求的 url 與所有配置的 perms 過濾器鏈進(jìn)行匹配,是則進(jìn)行權(quán)限檢查,不是則接著與下一個過濾器鏈進(jìn)行匹配,源碼如下:
protected boolean pathsMatch(String path, ServletRequest request) {
String requestURI = getPathWithinApplication(request);
log.trace("Attempting to match pattern '{}' with current requestURI '{}'...", path, requestURI);
return pathsMatch(path, requestURI);
}
方法
了解完這兩個方法,我來說說如何利用這兩個方法來實(shí)現(xiàn)功能。
我們可以從配置的過濾器鏈來入手,原先的配置如:
/users = perms[user:list]
我們可以改為 /user==GET,/user==POST 方式。== 用來分隔, 后面的部分指 HTTP Method。
使用這種方式還要注意一個方法,即:org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver 中的 getChain 方法,用來獲取當(dāng)前請求的 URL 應(yīng)該使用的過濾器,源碼如下:
public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
// 1. 判斷有沒有配置過濾器鏈, 沒有一個過濾器都沒有則直接返回 null
FilterChainManager filterChainManager = getFilterChainManager();
if (!filterChainManager.hasChains()) {
return null;
}
// 2. 獲取當(dāng)前請求的 URL
String requestURI = getPathWithinApplication(request);
// 3. 遍歷所有的過濾器鏈
for (String pathPattern : filterChainManager.getChainNames()) {
// 4. 判斷當(dāng)前請求的 URL 與過濾器鏈中的 URL 是否匹配.
if (pathMatches(pathPattern, requestURI)) {
if (log.isTraceEnabled()) {
log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "]. " +
"Utilizing corresponding filter chain...");
}
// 5. 如果路徑匹配, 則獲取其實(shí)現(xiàn)類.(如 perms[user:list] 或 perms[user:delete] 都返回 perms)
// 具體對 perms[user:list] 或 perms[user:delete] 的判斷是在上面講到的 PermissionsAuthorizationFilter 的 pathsMatch 方法中.
return filterChainManager.proxy(originalChain, pathPattern);
}
}
return null;
}
這里大家需要注意,第四步的判斷,我們已經(jīng)將過濾器鏈,也就是這里的 pathPattern 改為了 /xxx==GET 這種方式,而請求的 URL 卻僅包含 /xxx,那么這里的 pathMatches 方法是肯定無法匹配成功,所以我們需要在第四步判斷的時候,只判斷前面的 URL 部分。
整個過程如下:
在過濾器鏈上對 restful 請求配置需要的
HTTP Method,如:/user==DELETE。修改
PathMatchingFilterChainResolver的getChain方法,當(dāng)前請求的 URL 與過濾器鏈匹配時,過濾器只取 URL 部分進(jìn)行判斷。修改過濾器的
pathsMatch方法,判斷當(dāng)前請求的 URL 與請求方式是否與過濾器鏈中配置的一致。修改過濾器的
onAccessDenied方法,當(dāng)訪問被拒絕時,根據(jù)普通請求和AJAX請求分別返回HTML和JSON數(shù)據(jù)。
下面我們逐步來實(shí)現(xiàn):
實(shí)現(xiàn)
過濾器鏈添加 http method
在我的項(xiàng)目中是從數(shù)據(jù)庫獲取的過濾器鏈,所以有如下代碼:
public Map<String, String> getUrlPermsMap() {
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/favicon.ico", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/fonts/**", "anon");
filterChainDefinitionMap.put("/images/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/lib/**", "anon");
filterChainDefinitionMap.put("/login", "anon");
List<Menu> menus = selectAll();
for (Menu menu : menus) {
String url = menu.getUrl();
if (!"".equals(menu.getMethod())) {
url += ("==" + menu.getMethod());
}
String perms = "perms[" + menu.getPerms() + "]";
filterChainDefinitionMap.put(url, perms);
}
filterChainDefinitionMap.put("/**", "authc");
return filterChainDefinitionMap;
}
如:
/xxx==GET = perms[user:list]這里的getUrl,getMethod和getPerms分別對應(yīng)/xxx,GET和user:list。不過需要注意的是,如果在 XML 里配置,會被 Shiro 解析成
/xxx和=GET = perms[user:list],解決辦法是使用其他符號代替==。
修改 PathMatchingFilterChainResolver 的 getChain 方法
由于 Shiro 沒有提供相應(yīng)的接口,且我們不能直接修改源碼,所以我們需要新建一個類繼承 PathMatchingFilterChainResolver 并重寫 getChain 方法,然后替換掉 PathMatchingFilterChainResolver 即可。
首先繼承并重寫方法:
package im.zhaojun.shiro;
import org.apache.shiro.web.filter.mgt.FilterChainManager;
import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.FilterChain;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public class RestPathMatchingFilterChainResolver extends PathMatchingFilterChainResolver {
private static final Logger log = LoggerFactory.getLogger(RestPathMatchingFilterChainResolver.class);
@Override
public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
FilterChainManager filterChainManager = getFilterChainManager();
if (!filterChainManager.hasChains()) {
return null;
}
String requestURI = getPathWithinApplication(request);
//the 'chain names' in this implementation are actually path patterns defined by the user. We just use them
//as the chain name for the FilterChainManager's requirements
for (String pathPattern : filterChainManager.getChainNames()) {
String[] pathPatternArray = pathPattern.split("==");
// 只用過濾器鏈的 URL 部分與請求的 URL 進(jìn)行匹配
if (pathMatches(pathPatternArray[0], requestURI)) {
if (log.isTraceEnabled()) {
log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "]. " +
"Utilizing corresponding filter chain...");
}
return filterChainManager.proxy(originalChain, pathPattern);
}
}
return null;
}
}
然后替換掉 PathMatchingFilterChainResolver,它是在 ShiroFilterFactoryBean 的 createInstance 方法里初始化的。

所以同樣的套路,繼承 ShiroFilterFactoryBean 并重寫 createInstance 方法,將 new PathMatchingFilterChainResolver(); 改為 new RestPathMatchingFilterChainResolver(); 即可。
代碼如下:
package im.zhaojun.shiro;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.mgt.FilterChainManager;
import org.apache.shiro.web.filter.mgt.FilterChainResolver;
import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;
import org.apache.shiro.web.mgt.WebSecurityManager;
import org.apache.shiro.web.servlet.AbstractShiroFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanInitializationException;
public class RestShiroFilterFactoryBean extends ShiroFilterFactoryBean {
private static final Logger log = LoggerFactory.getLogger(RestShiroFilterFactoryBean.class);
@Override
protected AbstractShiroFilter createInstance() {
log.debug("Creating Shiro Filter instance.");
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();
//Expose the constructed FilterChainManager by first wrapping it in a
// FilterChainResolver implementation. The AbstractShiroFilter implementations
// do not know about FilterChainManagers - only resolvers:
PathMatchingFilterChainResolver chainResolver = new RestPathMatchingFilterChainResolver();
chainResolver.setFilterChainManager(manager);
//Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built
//FilterChainResolver. It doesn't matter that the instance is an anonymous inner class
//here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts
//injection of the SecurityManager and FilterChainResolver:
return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
}
private static final class SpringShiroFilter extends AbstractShiroFilter {
protected SpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) {
super();
if (webSecurityManager == null) {
throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
}
setSecurityManager(webSecurityManager);
if (resolver != null) {
setFilterChainResolver(resolver);
}
}
}
}
最后記得將 ShiroFilterFactoryBean 改為 RestShiroFilterFactoryBean。
XML 方式:
<bean id="shiroFilter" class="im.zhaojun.shiro.RestShiroFilterFactoryBean">
<!-- 參數(shù)配置略 -->
</bean>
Bean 方式:
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new RestShiroFilterFactoryBean();
// 參數(shù)配置略
return shiroFilterFactoryBean;
}
修改過濾器的 pathsMatch 方法
同樣新建一個類繼承原有的 PermissionsAuthorizationFilter 并重寫 pathsMatch 方法:
package im.zhaojun.shiro.filter;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.StringUtils;
import org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 修改后的 perms 過濾器, 添加對 AJAX 請求的支持.
*/
public class RestAuthorizationFilter extends PermissionsAuthorizationFilter {
private static final Logger log = LoggerFactory
.getLogger(RestAuthorizationFilter.class);
@Override
protected boolean pathsMatch(String path, ServletRequest request) {
String requestURI = this.getPathWithinApplication(request);
String[] strings = path.split("==");
if (strings.length <= 1) {
// 普通的 URL, 正常處理
return this.pathsMatch(strings[0], requestURI);
} else {
// 獲取當(dāng)前請求的 http method.
String httpMethod = WebUtils.toHttp(request).getMethod().toUpperCase();
// 匹配當(dāng)前請求的 http method 與 過濾器鏈中的的是否一致
return httpMethod.equals(strings[1].toUpperCase()) && this.pathsMatch(strings[0], requestURI);
}
}
}
修改過濾器的 onAccessDenied 方法
同樣是上一步的類,重寫 onAccessDenied 方法即可:
/**
* 當(dāng)沒有權(quán)限被攔截時:
* 如果是 AJAX 請求, 則返回 JSON 數(shù)據(jù).
* 如果是普通請求, 則跳轉(zhuǎn)到配置 UnauthorizedUrl 頁面.
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
Subject subject = getSubject(request, response);
// 如果未登錄
if (subject.getPrincipal() == null) {
// AJAX 請求返回 JSON
if (im.zhaojun.util.WebUtils.isAjaxRequest(WebUtils.toHttp(request))) {
if (log.isDebugEnabled()) {
log.debug("用戶: [{}] 請求 restful url : {}, 未登錄被攔截.", subject.getPrincipal(), this.getPathWithinApplication(request)); }
Map<String, Object> map = new HashMap<>();
map.put("code", -1);
im.zhaojun.util.WebUtils.writeJson(map, response);
} else {
// 其他請求跳轉(zhuǎn)到登陸頁面
saveRequestAndRedirectToLogin(request, response);
}
} else {
// 如果已登陸, 但沒有權(quán)限
// 對于 AJAX 請求返回 JSON
if (im.zhaojun.util.WebUtils.isAjaxRequest(WebUtils.toHttp(request))) {
if (log.isDebugEnabled()) {
log.debug("用戶: [{}] 請求 restful url : {}, 無權(quán)限被攔截.", subject.getPrincipal(), this.getPathWithinApplication(request));
}
Map<String, Object> map = new HashMap<>();
map.put("code", -2);
map.put("msg", "沒有權(quán)限啊!");
im.zhaojun.util.WebUtils.writeJson(map, response);
} else {
// 對于普通請求, 跳轉(zhuǎn)到配置的 UnauthorizedUrl 頁面.
// 如果未設(shè)置 UnauthorizedUrl, 則返回 401 狀態(tài)碼
String unauthorizedUrl = getUnauthorizedUrl();
if (StringUtils.hasText(unauthorizedUrl)) {
WebUtils.issueRedirect(request, response, unauthorizedUrl);
} else {
WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
}
return false;
}
重寫完 pathsMatch 和 onAccessDenied 方法后,將這個類替換原有的 perms 過濾器的類:
XML 方式:
<bean id="shiroFilter" class="im.zhaojun.shiro.RestShiroFilterFactoryBean">
<!-- 參數(shù)配置略 -->
<property name="filters">
<map>
<entry key="perms" value-ref="restAuthorizationFilter"/>
</map>
</property>
</bean>
<bean id="restAuthorizationFilter" class="im.zhaojun.shiro.filter.RestAuthorizationFilter"/>
Bean 方式:
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new RestShiroFilterFactoryBean();
Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
filters.put("perms", new RestAuthorizationFilter());
// 其他配置略
return shiroFilterFactoryBean;
}
這里只改了
perms過濾器,對于其他過濾器也是同樣的道理,重寫過濾器的pathsMatch和onAccessDenied方法,并覆蓋原有過濾器即可。
結(jié)語
基本的過程就是這些,這是我在學(xué)習(xí) Shiro 的過程中的一些見解,希望可以幫助到大家。具體應(yīng)用的項(xiàng)目地址為:https://github.com/zhaojun1998/Shiro-Action,功能在不斷完善中,代碼可能有些粗糙,還請見諒。