Shiro-受權(RBAC)

0. 前言

[Shiro-認證]中講解了如何使用Shiro實現登陸後訪問URL, 對於大部分系統來講, 登陸只是安全的第一道屏障, 系統中的某些頁面須要登陸後訪問, 而有些是須要有特定權限才能夠訪問, 好比刪除, 凍結, 查看帳號收益等敏感的操做.git

本文將帶你實現基於Shiro的權限控制, Shiro中叫作受權github

1. 什麼是權限

系統中有A,B,C三個用戶, 其中A用戶是管理員, B和C是普通用戶. 系統中的全部刪除操做必須由管理員帳號登陸才能完成. 普通用戶是沒法刪除數據甚至連刪除按鈕都看不見. 咱們說A,B,C三個用戶在系統中有不一樣的權限. A有刪除數據的權限, B和C沒有刪除數據的權限. 試想一下若是沒有權限設計, 全部用戶均可以刪除數據, 假設B是新手不當心誤操做刪除了數據... 後果將不堪設想.web

又例如銀行的金庫, 若是沒有權限控制全部人均可以刷卡進入, 那豈不是要亂套. 生活中權限無處不在: 進出小區刷卡, 電梯刷卡到指定樓層, 視頻網站中會員不須要看廣告, 這些都是權限.數據庫

2. 權限設計方案

假如你作了一個交友網站, 裏面有查看異性的基本信息, 查看微信, 查看電話, 查看家庭住址幾個功能, 普通的用戶只能查看基本信息, 不能查看聯繫方式等. 充值100元能夠查看微信, 充值200元能夠查看電話, 充值500元能夠查看家庭住址.apache

你必需要作權限控制, 不然用戶經過其餘手段(好比知道URL)就能夠查看聯繫方式, 也就沒有人給你付費了. 最初, 你可能想到這麼處理權限: 用一張數據表記錄每一個用戶能夠作什麼事. 當用戶查看微信時找到登陸用戶的權限判斷是否能夠查看微信.瀏覽器

用戶 基本信息 查看微信 查看電話 查看住址
張三
李四
王五

隨着時間的增長會員愈來愈多, 有一天你新加了一個功能: 查看對方視頻介紹, 只有充值500的人才能查看. 因而你須要把上表中全部用戶的權限都修改一遍. 若是有幾十萬會員, 可能你就會累到吐血....安全

聰明的你想到了一個辦法, 設置會員等級, 充值100爲普通會員, 充值200元爲VIP, 充值500爲VIP中P. 給每個會員設置會員等級. 此時你的數據表結構以下:bash

  • 會員等級-權限
會員等級 基本信息 查看微信 查看電話 查看住址 查看視頻
充值100元: 初級會員
充值200元: VIP
充值500元: VIP中P
  • 會員-會員等級
用戶名 會員等級
張三 普通會員
李四 VIP
王五 VIP中P
趙六 VIP中P

這時, 當用戶查看微信時, 根據用戶找到會員等級, 在找到對應的權限. 雖然多了一步操做, 但:微信

  • 新加入功能時, 只須要對會員等級設置相應的權限便可. 不須要對用戶進行權限設置
  • 用戶會員等級升級時, 修改用戶的會員等級便可. 不須要額外修改會員的權限
  • 會員等級的權限須要發生變化時, 只須要修改會員等級對應的權限, 對會員沒有影響

總之, 權限只針對會員等級, 和會員並沒有直接關聯. 這裏的會員等級就至關於系統中的角色, 基於角色的權限方案被不少系統所採用, 有了一個專有名詞: RBAC-基於角色的權限訪問控制.app

通俗的說就是根據用戶的角色來判斷是否有權限訪問某個資源或URL. RBAC的模型是經典的5張表:

  • 用戶: 記錄系統的用戶信息, 登陸時使用. 例: 張三, 李四...
  • 角色: 記錄系統中存在的角色. 例: 普通會員, VIP...
  • 資源: 記錄系統中的有哪些能夠作的事. 在WEB中對應的就是URL, 例: 查看資料URL, 查看微信URL, 查看電話....
  • 用戶角色關係: 記錄用戶所屬的角色, 例: 張三是普通會員, 李四是VIP...
  • 角色資源關係: 記錄了某個角色能夠作什麼事, 例: VIP能夠查看資料, 查看微信...

系統預先設計好角色, 資源, 角色資源關係. 當新建用戶時只須要添加用戶角色關係便可實現對該用戶的權限控制. 例如: 孫七註冊了用戶並充值200元, 咱們能夠直接設置孫七爲VIP, 經過孫七的角色VIP就能夠從角色資源關係中找到對應的可操做的URL.

3. Shiro中實現RBAC

3.0 內置過濾器

本文中的操做是基於[Shiro-認證]之上完成的, 建議先看完Shiro認證部分. Shiro的認證是經過內置的認證過濾器(authc)完成的, 同時也提供了一些受權相關的過濾器:

3.0.1 端口過濾器: port

訪問的端口不是定義的端口時重定向至定義的端口,對應類爲org.apache.shiro.web.filter.authz.PortFilter

filterChainDefinitionMap = [
    "/**" : "port[9090]" // 若是不是經過9090端口將會重定向至9090端口訪問
]
複製代碼

訪問http://localhost:8080/user/list, 端口爲8080, 該請求被port過濾器攔截, 重定向至9090端口, 即http://localhost:9090/user/list, port過濾器適用於項目端口變動期間兼容原有用戶訪問或將老版本系統自動切換到新版本(8080部署老版本, 9090部署新版本)

3.0.2 SSL過濾器: ssl

非https訪問443端口時, 重定向使用https訪問443端口. 對應類爲org.apache.shiro.web.filter.authz.SslFilter

filterChainDefinitionMap = [
    "/**" : "ssl" // 不能夠設置端口號,非https訪問443端口會被重定向以https方式訪問443端口
]
複製代碼

訪問http://localhost:456/user/list, 因爲http方式訪問456端口, 該請求被port過濾器攔截重定向至https://localhost/user/list(80,443端口默認不顯示), 適用於新增SSL證書後須要https訪問, 兼容原有使用http訪問的用戶.

3.0.3 角色過濾器: roles

用戶必須具備配置的角色才能夠訪問. Shiro會調用Realm中查詢受權信息的方法獲取用戶的角色. 對應類爲org.apache.shiro.web.filter.authz.RolesAuthorizationFilter

filterChainDefinitionMap = [
    "/**" : "roles['admin,guest']" // 訪問用戶必須同時具有admin和guest角色才能夠訪問
]
複製代碼

如配置成roles["admin"]表明只要是admin角色就能夠訪問, 兩個及以上角色表明必須同時知足.

3.0.4 權限過濾器: perms

filterChainDefinitionMap = [
    "/user/add" : "perms['user:add']" // 訪問用戶必須擁有user模塊的add權限
]
複製代碼

用戶必須具備配置的權限才能夠訪問, Shiro會調用Realm中查詢受權信息的方法查詢用戶的權限. 對應類爲org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter

user:add表明user模塊的add權限, 權限設計能夠按模塊劃分並細化到模塊的每一個功能點, 好比用戶(user)模塊中Admin角色有添加(add)用戶權限, 刪除(delete)用戶權限, 數據庫中可存儲Admin擁有的權限爲user:add, user:delete, 當訪問/user/add請求時, Shiro會經過Realm獲取對應的權限, 若是含有user:add便可訪問該請求, 沒有該權限禁止訪問.

如shiro中只配置到模塊級別可使用user:*進行通配符驗證. perms[user:*:add]表明訪問權限爲user模塊下全部子模塊(*匹配子模塊)的添加(add)權限

3.0.5 REST風格權限過濾器: rest

對應類爲org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter

filterChainDefinitionMap = [
    // 訪問用戶必須擁有user模塊的對應權限, GET請求表明read
    // 已GET方式的請求必須擁有user:read權限才能夠訪問
    "/user/*" : "rest[user]" 
]
複製代碼

將請求方式與增刪改查操做對應, 當以POST方式訪問URL時, 過濾器認爲須要對模塊進行create操做, 用戶必須擁有user:create權限, 不一樣的請求方式對應不一樣的權限. 具體以下表:

HTTP請求方式 Shiro對應的操做 系統中須要授予用戶的權限(以user模塊爲例)
delete delete user:delete
head read user:read
get read user:read
put update user:update
post create user:create
mkcol create user:create
options read user:read
trace read user:read

此過濾器將http請求方式和權限進行綁定, 能夠算是perms過濾器的另外一種實現方式. 因爲瀏覽器對部分HTTP請求方式支持的不友好, 此過濾器應用較少.

3.1 自定義權限過濾器

上述內置過濾器中能夠支持RBAC的有roles, perms, rest, 其中roles只定義了角色, perms, rest的規則也是須要在Shiro配置文件中進行配置模塊及權限. 若是系統增長功能並設置權限時還須要同步修改配置文件(修改後須要從新啓動Tomcat). 有沒有一種靈活的方式能夠實現增長功能時不須要修改系統代碼呢, 參考下面的思路:

WEB應用中全部的操做都是基於URL的, 例如: /user/add是添加用戶, /article/delete是刪除文章. 若是咱們將URL設置給角色. 當用戶訪問某一個URL時, 咱們只須要對比該用戶擁有的權限集中是否含有該URL便可.

例: 張三的角色爲部門經理, 擁有添加用戶(/user/add)和編輯用戶(/user/edit)權限, 當張三登陸系統後訪問/user/add, 經過Realm獲取張三的權限後對比發現URL(/user/add)在其權限列表中, Shiro容許訪問. 當訪問/user/delete時因爲URL不在其權限中, 所以Shiro拒絕訪問.

全部的URL請求都使用上述方式實現, 配置文件中就不須要定義每一個URL對應的權限了. 所以新增功能時也就不須要修改系統代碼了.

Shiro並無內置這種形式的過濾器, 須要咱們本身實現, 新建類繼承AuthorizationFilter類重寫isAccessAllowed方法. 後面文章會講到isAccessAllowed是Shiro過濾器的一個核心方法: 判斷當前過濾器的驗證是否成功, 若是成功則放行(訪問控制器).

  • 認證過濾器: 驗證指的是否已經登陸
  • 受權過濾器: 驗證指的是用戶是否有權限訪問
/**
 * 自定義基於URL的受權過濾器
 * 經過用戶訪問的URL,從數據庫中查詢用戶是否有訪問該URL的權限
 */
public class URLAuthorizationFilter extends AuthorizationFilter {

    /**
     * 是否容許訪問資源
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, 
                                      ServletResponse response, 
                                      Object mappedValue) throws Exception {

        // 獲取訪問的URL
        String requestUrl = WebUtils.toHttp(request).getRequestURI();
        // 判斷用戶是否有權限訪問該URL
        // 調用isPermitted方法時Shiro會經過Realm獲取用戶擁有的權限集合
        // 並判斷URL是否在權限集合中, 若是在權限集合中返回true
        return getSubject(request, response).isPermitted(requestUrl);

    }

}
複製代碼

3.2 配置權限過濾器

自定義的過濾器須要在Shiro中進行定義, 並配置URL須要受權才能訪問

// 配置自定義過濾器,名稱爲authz
authz(URLAuthorizationFilter) {
    // 無權限頁面: 用戶無權限時重定向至該頁面
    unauthorizedUrl = "/unauthorized.jsp"
}
複製代碼
// 配置URL規則
// 有請求訪問時Shiro會根據此規則找到對應的過濾器處理
filterChainDefinitionMap = [
    "/unauthorized.jsp" : "anon", // 未受權頁不須要受權便可訪問
    "/logout" : "logout", // 登出使用logout過濾器
    "/login": "authc", // 登陸頁不配置受權
    "/**": "authc, authz" // 其他全部頁面須要認證和受權(順序:先認證後受權)
]
複製代碼
  • 受權和認證的順序, 先認證後受權, 若是用戶未登陸, 沒法獲取用戶所擁有的權限信息(受權時發現未認證會跳轉登陸頁).
  • 登陸頁不須要受權: authc不會處理登陸頁, 若是配置到authz中, 會出現死循環(authz認爲未登陸重定向到登陸頁)

3.3 Realm實現獲取權限

Shiro須要使用Realm獲取用戶的權限集合, 所以須要在Realm中增長一個獲取權限的方法

// 自定義查詢用戶信息的Realm
// 受權須要繼承AuthorizingRealm(只認證繼承AuthenticatingRealm便可)
public class UserRealm extends AuthorizingRealm {

    // 獲取用戶權限信息
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 獲取當前登陸用戶的用戶名
        // Shiro會將doGetAuthenticationInfo返回的用戶信息保存至PrincipalCollection中
        String username = ((User) principals.getPrimaryPrincipal()).getUsername();
        // 模擬數據庫查詢, 根據用戶名查詢能夠訪問的權限URL集合
        Set<String> permSet = getPermissions(username);

        // 將權限URL集合設置至Shiro中,受權時會今後處獲取權限URL
        SimpleAuthorizationInfo authz = new SimpleAuthorizationInfo();
        authz.setStringPermissions(permSet);

        return authz;
    }

    // 模擬根據用戶名在數據庫中查詢用戶全部的權限URL
    // 數據庫中可根據用戶找到角色,角色找到資源
    private Set<String> getPermissions(String username) {
        Set<String> permSet = new HashSet<String>();

        // "atd681"有下列頁面的訪問權限
        if ("atd681".equals(username)) {
            permSet.add("/page/a");
            permSet.add("/page/b");
        }
        // 其餘用戶有下列頁面的訪問權限
        else {
            permSet.add("/page/x");
        }

        return permSet;
    }

    // 獲取用戶信息的方法
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
        throws AuthenticationException {
       // shiro-認證中的登陸邏輯
    }

    // 模擬根據用戶名在數據庫查詢用戶信息
    private User getUser(String username) {
        // shiro-認證中的模擬獲取用戶信息
    }

}
複製代碼
  • 獲取權限信息的Realm必須繼承AuthorizingRealm實現doGetAuthorizationInfo方法
  • 獲取權限時根據關係(用戶>角色>資源)找到用戶所擁有的資源(可訪問的URL)
  • 須要將獲取的權限集合(Set)設置到SimpleAuthorizationInfo類中並返回至Shiro
  • 本例中用戶atd681能夠訪問a,b頁面, 不可訪問x頁面

4. 測試

啓動項目, 使用atd681登陸後分別訪問/page/a/page/b

能夠正常訪問. 訪問/page/x時重定向至未受權頁面

5. 視圖層控制權限

上述權限控制是當用戶訪問URL時在服務端進行受權校驗. 在頁面中咱們並無根據權限控制連接或按鈕是否顯示, 不控制連接或按鈕的顯示會存在如下問題:

  • 用戶無權限時點擊連接或按鈕後沒法訪問, 用戶體驗較差.
  • 暴露了系統該功能的URL, 引發沒必要要的安全隱患

所以, 當用戶沒有某功能權限時頁面中不該該顯示功能對應的連接或按鈕(刻意顯示連接吸引用戶付費等場景除外), 咱們須要在JSP中對連接或按鈕進行權限判斷, 沒有權限時不顯示對應的連接或按鈕.

Shiro爲咱們提供了一套在JSP中能夠判斷認證或受權的標籤, 在/page/a的JSP中添加以下代碼:

JSP頭部增長Shiro標籤的引用

<%@ tagliburi ="http://shiro.apache.org/tags" prefix="shiro"%>
複製代碼

JSP中使用shiro:hasPermission根據用戶的權限來控制是否顯示連接或按鈕

<body>
	系統菜單:
	
	<!-- 
		該標籤根據name值判斷當前用戶是否有該頁面的訪問權限
		無權限時不顯示該連接(調用subject.isPermitted方法進行驗證)
	 -->
	<shiro:hasPermission name="/page/a">
		<a href="/page/a">A</a>
	</shiro:hasPermission>
	<shiro:hasPermission name="/page/b">
		<a href="/page/b">B</a>
	</shiro:hasPermission>
	<shiro:hasPermission name="/page/x">
		<a href="/page/x">X</a>
	</shiro:hasPermission>
	
	
	<br> PAGE_A, 當前登陸用戶ID: ${userId}, 用戶名: ${userName}

	<a href="/logout">登出</a>
</body>
複製代碼
  • <shiro:hasPermission>中的name屬性爲連接的URL, 判斷用戶是否有權限訪問URL
  • 只有當<shiro:hasPermission>返回true的時候, 標籤內的HTML纔會被返回到客戶端
  • 全部標籤的實現代碼在org.apache.shiro.web.tags目錄下, 有興趣能夠本身查看

啓動項目, 使用atd681登陸後訪問/page/a, 因爲用戶atd681有訪問/page/a/page/b的權限, 連接A,B被顯示. 沒有訪問/page/x的權限, 連接X沒有顯示.

6. 示例代碼

至此, 基於Shiro受權的示例配置完成. 有興趣的同窗能夠多建立幾個用戶測試一下.

相關文章
相關標籤/搜索