Shiro 拓展之 Restful URL 鑒權(quán)

前言

在使用 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 部分。

整個過程如下:

  1. 在過濾器鏈上對 restful 請求配置需要的 HTTP Method,如:/user==DELETE。

  2. 修改 PathMatchingFilterChainResolvergetChain 方法,當(dāng)前請求的 URL 與過濾器鏈匹配時,過濾器只取 URL 部分進(jìn)行判斷。

  3. 修改過濾器的 pathsMatch 方法,判斷當(dāng)前請求的 URL 與請求方式是否與過濾器鏈中配置的一致。

  4. 修改過濾器的 onAccessDenied 方法,當(dāng)訪問被拒絕時,根據(jù)普通請求和 AJAX 請求分別返回 HTMLJSON 數(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]這里的 getUrlgetMethodgetPerms 分別對應(yīng) /xxx,GETuser: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,它是在 ShiroFilterFactoryBeancreateInstance 方法里初始化的。

image

所以同樣的套路,繼承 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;
}

重寫完 pathsMatchonAccessDenied 方法后,將這個類替換原有的 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 過濾器,對于其他過濾器也是同樣的道理,重寫過濾器的 pathsMatchonAccessDenied 方法,并覆蓋原有過濾器即可。

結(jié)語

基本的過程就是這些,這是我在學(xué)習(xí) Shiro 的過程中的一些見解,希望可以幫助到大家。具體應(yīng)用的項(xiàng)目地址為:https://github.com/zhaojun1998/Shiro-Action,功能在不斷完善中,代碼可能有些粗糙,還請見諒。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容