Shiro-授权(RBAC)
0. 前言
在[Shiro-認證]中講解了如何使用Shiro實現登錄后訪問URL, 對于大部分系統來說, 登錄只是安全的第一道屏障, 系統中的某些頁面需要登錄后訪問, 而有些是需要有特定權限才可以訪問, 比如刪除, 凍結, 查看賬號收益等敏感的操作.
本文將帶你實現基于Shiro的權限控制, Shiro中叫做授權
1. 什么是權限
系統中有A,B,C三個用戶, 其中A用戶是管理員, B和C是普通用戶. 系統中的所有刪除操作必須由管理員賬號登錄才能完成. 普通用戶是無法刪除數據甚至連刪除按鈕都看不見. 我們說A,B,C三個用戶在系統中有不同的權限. A有刪除數據的權限, B和C沒有刪除數據的權限. 試想一下如果沒有權限設計, 所有用戶都可以刪除數據, 假設B是新手不小心誤操作刪除了數據... 后果將不堪設想.
又例如銀行的金庫, 如果沒有權限控制所有人都可以刷卡進入, 那豈不是要亂套. 生活中權限無處不在: 進出小區刷卡, 電梯刷卡到指定樓層, 視頻網站中會員不需要看廣告, 這些都是權限.
2. 權限設計方案
假如你做了一個交友網站, 里面有查看異性的基本信息, 查看微信, 查看電話, 查看家庭住址幾個功能, 普通的用戶只能查看基本信息, 不能查看聯系方式等. 充值100元可以查看微信, 充值200元可以查看電話, 充值500元可以查看家庭住址.
你必須要做權限控制, 否則用戶通過其他手段(比如知道URL)就可以查看聯系方式, 也就沒有人給你付費了. 最初, 你可能想到這么處理權限: 用一張數據表記錄每個用戶可以做什么事. 當用戶查看微信時找到登錄用戶的權限判斷是否可以查看微信.
| 張三 | √ | |||
| 李四 | √ | √ | ||
| 王五 | √ | √ | √ | √ |
隨著時間的增加會員越來越多, 有一天你新加了一個功能: 查看對方視頻介紹, 只有充值500的人才能查看. 于是你需要把上表中所有用戶的權限都修改一遍. 如果有幾十萬會員, 可能你就會累到吐血....
聰明的你想到了一個辦法, 設置會員等級, 充值100為普通會員, 充值200元為VIP, 充值500為VIP中P. 給每一個會員設置會員等級. 此時你的數據表結構如下:
- 會員等級-權限
| 充值100元: 初級會員 | √ | ||||
| 充值200元: VIP | √ | √ | √ | ||
| 充值500元: VIP中P | √ | √ | √ | √ | √ |
- 會員-會員等級
| 張三 | 普通會員 |
| 李四 | VIP |
| 王五 | VIP中P |
| 趙六 | VIP中P |
這時, 當用戶查看微信時, 根據用戶找到會員等級, 在找到對應的權限. 雖然多了一步操作, 但:
- 新加入功能時, 只需要對會員等級設置相應的權限即可. 不需要對用戶進行權限設置
- 用戶會員等級升級時, 修改用戶的會員等級即可. 不需要額外修改會員的權限
- 會員等級的權限需要發生變化時, 只需要修改會員等級對應的權限, 對會員沒有影響
總之, 權限只針對會員等級, 和會員并無直接關聯. 這里的會員等級就相當于系統中的角色, 基于角色的權限方案被很多系統所采用, 有了一個專有名詞: RBAC-基于角色的權限訪問控制.
通俗的說就是根據用戶的角色來判斷是否有權限訪問某個資源或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權限, 不同的請求方式對應不同的權限. 具體如下表:
| 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過濾器的一個核心方法: 判斷當前過濾器的驗證是否成功, 如果成功則放行(訪問控制器).
- 認證過濾器: 驗證指的是否已經登錄
- 授權過濾器: 驗證指的是用戶是否有權限訪問
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 {// 獲取用戶權限信息@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {// 獲取當前登錄用戶的用戶名// Shiro會將doGetAuthenticationInfo返回的用戶信息保存至PrincipalCollection中String username = ((User) principals.getPrimaryPrincipal()).getUsername();// 模擬數據庫查詢, 根據用戶名查詢可以訪問的權限URL集合Set<String> permSet = getPermissions(username);// 將權限URL集合設置至Shiro中,授權時會從此處獲取權限URLSimpleAuthorizationInfo 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;}// 獲取用戶信息的方法@Overrideprotected 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授權的示例配置完成. 有興趣的同學可以多創建幾個用戶測試一下.
- 示例代碼地址: github.com/atd681/alld…
- 示例項目名稱: atd681-shiro-authz
轉載于:https://juejin.im/post/5b75694ee51d456660166b45
總結
以上是生活随笔為你收集整理的Shiro-授权(RBAC)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: PDManer 入门教程:超强代码生成工
- 下一篇: Week08_day07(DataX从m