1. 程式人生 > >shiro實現方法級別的細粒度url許可權控制

shiro實現方法級別的細粒度url許可權控制

關於 Shiro 的許可權匹配器和過濾器

上一節,我們實現了自定義的 Realm,方式是繼承 AuthorizingRealm 這個抽象類,分別實現認證的方法和授權的方法。

這一節實現的程式碼的執行順序:
1、Shiro定義的過濾器和自定義的過濾器,在自定義的過濾器中執行 Subject 物件的判斷是否具有某項許可權的方法 isPermitted() ,傳入某一個跟當前登入物件相關的特徵值(這裡是登入物件正在訪問的 url 連線)
2、程式走到自定義的 Realm 中的授權方法中,根據已經認證過的主體查詢該主體具有的角色和許可權字串,通常情況下是一個角色的集合和一個許可權的集合。

此時我們第 1 步有一個字串,第 2 步有一個字串的集合(許可權的集合)。
程式要幫我們做的就是看第 1 步的字串在不在第 2 步的字串集合中。那麼這件事情是如何實現的呢?

3、此時程式檢測到配置檔案中有宣告一個實現了 PermissionResolver 的類,這個時候程式就會到這個類中去查詢所採用的許可權匹配策略。
4、到上一步返回的實現了 Permission 的類的物件中的 implies() 方法中去進行判斷。如果第 2 步的許可權字串數量多於 1 個,這個 implies() 就會執行多次,直到該方法返回 true 為止,第 1 步的 isPermitted() 才會返回 true。

下面我們來關注一下 [urls] 這個節點下面的部分。

[urls]
# 配置 url 與使用的過濾器之間的關係
/admin/**=authc,resourceCheckFilter
/login=anon
  • 1
  • 2
  • 3
  • 4

其中

/admin/**=authc,resourceCheckFilter
  • 1

表示,當請求 /admin/** 的時候,會依次經過 (1)authc 和 (2)resourceCheckFilter 這兩個過濾器。

過濾器在有些地方也叫攔截器,他們的意思是一樣的。

(1)authc 這個過濾器是 Shiro 自定義的認證過濾器,即到自定義 Realm 的認證方法裡面去按照指定的規則進行使用者名稱和密碼的匹配。
DefaultFilter 這個列舉類裡面定義了多個自定義的過濾器,可以直接使用。

(2)resourceCheckFilter 是一個自定義的過濾器,我們來看看它的宣告:

[filters]
# 宣告一個自定義的過濾器
resourceCheckFilter = com.liwei.shiro.filter.ResourceCheckFilter
# 為上面宣告的自定義過濾器注入屬性值
resourceCheckFilter.errorUrl=/unAuthorization
  • 1
  • 2
  • 3
  • 4
  • 5

實現:

public classResourceCheckFilterextendsAccessControlFilter {

    private String errorUrl;

    public String getErrorUrl() {
        return errorUrl;
    }

    public void setErrorUrl(String errorUrl) {
        this.errorUrl = errorUrl;
    }

    private static final Logger logger = LoggerFactory.getLogger(ResourceCheckFilter.class);

    /**
     * 表示是否允許訪問 ,如果允許訪問返回true,否則false;
     * @param servletRequest
     * @param servletResponse
     * @param o 表示寫在攔截器中括號裡面的字串 mappedValue 就是 [urls] 配置中攔截器引數部分
     * @return
     * @throws Exception
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
        Subject subject = getSubject(servletRequest,servletResponse);
        String url = getPathWithinApplication(servletRequest);
        logger.debug("當前使用者正在訪問的 url => " + url);
        return subject.isPermitted(url);
    }


    /**
     * onAccessDenied:表示當訪問拒絕時是否已經處理了;如果返回 true 表示需要繼續處理;如果返回 false 表示該攔截器例項已經處理了,將直接返回即可。

     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        logger.debug("當 isAccessAllowed 返回 false 的時候,才會執行 method onAccessDenied ");

        HttpServletRequest request =(HttpServletRequest) servletRequest;
        HttpServletResponse response =(HttpServletResponse) servletResponse;
        response.sendRedirect(request.getContextPath() + this.errorUrl);

        // 返回 false 表示已經處理,例如頁面跳轉啥的,表示不在走以下的攔截器了(如果還有配置的話)
        return false;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

注意:我們首先要關注 isAccessAllowed() 方法,在這個方法中,如果返回 true,則表示“通過”,走到下一個過濾器。如果沒有下一個過濾器的話,表示具有了訪問某個資源的許可權。如果返回 false,則會呼叫 onAccessDenied 方法,去實現相應的當過濾不通過的時候執行的操作,例如跳轉到某一個指定的登入頁面,去引導使用者輸入另一個具有更大許可權的使用者名稱和密碼進行登入。

isAccessAllowed() 方法的最後一個引數 o,可以獲得我們自定義的過濾器後面中括號中所帶的引數。

我們再跳回到 isAccessAllowed() 中:subject.isPermitted(url)。說明通過繼承 AccessControlFilter 我們可以得到認證主體 Subject 和當前請求的 url 連結,它們的 API 分別是:

獲得認證主體:

Subject subject = getSubject(servletRequest,servletResponse);
  • 1


獲得當前請求的 url

String url = getPathWithinApplication(servletRequest);
  • 1

然後,我們呼叫了 subject.isPermitted(url) 方法,將 url 這個字串物件傳入。

此時我們的流程應該走到 Realm 的授權方法中,通過查詢(經過了認證的)使用者資訊去查詢該使用者具有的許可權資訊。此時的程式碼走到了這裡。
在授權方法中,我們看到 SimpleAuthorizationInfo 的角色資訊和許可權資訊都是通過字串來解析的。
角色資訊和許可權資訊都是集合。

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    logger.info("--- MyRealm doGetAuthorizationInfo ---");

    // 獲得經過認證的主體資訊
    User user = (User)principalCollection.getPrimaryPrincipal();

    //
    // 此處為節約篇幅,突出重點省略
    //

    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    info.setRoles(new HashSet<>(roleSnList));
    info.setStringPermissions(new HashSet<>(resStrList));

    // 以上完成了動態地對使用者授權
    logger.debug("role => " + roleSnList);
    logger.debug("permission => " + resStrList);

    return info;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

在這個 Realm 的授權方法中,完成了對該認證後的主體所具有的角色和許可權的查詢,然後放入 SimpleAuthorizationInfo 物件中。

接下來就要進行 subject.isPermitted(url) 中的 url 和 自定義 Realm 中的授權方法中的 info.setStringPermissions(new HashSet<>(resStrList)); 許可權字串集合的匹配操作了。

許可權資訊:
這裡寫圖片描述
它們都是從資料庫中查詢出來的。

那麼如何實現匹配呢?比較簡單的一個思路就是比較字串,但是這件簡單的比較的事情被 Shiro 定義為一個 PermissionResolver ,通過實現 PermissionResolver ,我們可以為完成自定義的許可權匹配操作,可以是簡單的字串匹配,也可以稍有靈活性的萬用字元匹配,這都取決於我們程式設計師自己。

public classUrlPermissionResolverimplementsPermissionResolver {

    private static final Logger logger = LoggerFactory.getLogger(UrlPermissionResolver.class);

    /**
     * 經過除錯發現
     * subject.isPermitted(url) 中傳入的字串
     * 和自定義 Realm 中傳入的許可權字串集合都要經過這個 resolver
     * @param s
     * @return
     */
    @Override
    public Permission resolvePermission(String s) {
        logger.debug("s => " + s);

        if(s.startsWith("/")){
            return new UrlPermission(s);
        }
        return new WildcardPermission(s);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

可以看到,許可權資訊是通過字串:“/admin/**”等來進行匹配的。這時就不能使用 Shiro 預設的許可權匹配器 WildcardPermission 了。

而 UrlPermission 是一個實現了 Permission 介面的類,它的 implies 方法的實現決定了許可權是否匹配,所以 implies 這個方法的實現是很重要的。

public classUrlPermissionimplementsPermission {

    private static final Logger logger = LoggerFactory.getLogger(UrlPermission.class);

    // 在 Realm 的授權方法中,由資料庫查詢出來的許可權字串
    private String url;

    public UrlPermission(String url){
        this.url = url;
    }

    /**
     * 一個很重要的方法,使用者判斷 Realm 中設定的許可權和從資料庫或者配置檔案中傳遞進來的許可權資訊是否匹配
     * 如果 Realm 的授權方法中,一個認證主體有多個許可權,會進行遍歷,直到匹配成功為止
     * this.url 是在遍歷狀態中變化的
     *
     * urlPermission.url 是從 subject.isPermitted(url)
     * 傳遞到 UrlPermissionResolver 中傳遞過來的,就一個固定值
     *
     * @param permission
     * @return
     */
    @Override
    public boolean implies(Permission permission) {
        if(!(permission instanceof UrlPermission)){
            return false;
        }
        //
        UrlPermission urlPermission = (UrlPermission)permission;
        PatternMatcher patternMatcher = new AntPathMatcher();

        logger.debug("this.url(來自資料庫中存放的萬用字元資料),在 Realm 的授權方法中注入的 => " + this.url);
        logger.debug("urlPermission.url(來自瀏覽器正在訪問的連結) => " +  urlPermission.url);
        boolean matches = patternMatcher.matches(this.url,urlPermission.url);
        logger.debug("matches => " + matches);
        return matches;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

重點說明:如果在自定義的 Realm 中的授權方法中傳入的授權資訊中的許可權資訊是一個集合,那麼這裡的 implies 就會進行遍歷,直到這個方法返回 true 為止,如果遍歷的過程全部返回 false,就說明該認證主體不具有訪問某個資源的許可權。