在[Shiro-認證]中講解了如何使用Shiro實現登陸後訪問URL, 對於大部分系統來講, 登陸只是安全的第一道屏障, 系統中的某些頁面須要登陸後訪問, 而有些是須要有特定權限才能夠訪問, 好比刪除, 凍結, 查看帳號收益等敏感的操做.git
本文將帶你實現基於Shiro的權限控制, Shiro中叫作受權github
系統中有A,B,C三個用戶, 其中A用戶是管理員, B和C是普通用戶. 系統中的全部刪除操做必須由管理員帳號登陸才能完成. 普通用戶是沒法刪除數據甚至連刪除按鈕都看不見. 咱們說A,B,C三個用戶在系統中有不一樣的權限. A有刪除數據的權限, B和C沒有刪除數據的權限. 試想一下若是沒有權限設計, 全部用戶均可以刪除數據, 假設B是新手不當心誤操做刪除了數據... 後果將不堪設想.web
又例如銀行的金庫, 若是沒有權限控制全部人均可以刷卡進入, 那豈不是要亂套. 生活中權限無處不在: 進出小區刷卡, 電梯刷卡到指定樓層, 視頻網站中會員不須要看廣告, 這些都是權限.數據庫
假如你作了一個交友網站, 裏面有查看異性的基本信息, 查看微信, 查看電話, 查看家庭住址幾個功能, 普通的用戶只能查看基本信息, 不能查看聯繫方式等. 充值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張表:
系統預先設計好角色, 資源, 角色資源關係. 當新建用戶時只須要添加用戶角色關係便可實現對該用戶的權限控制. 例如: 孫七註冊了用戶並充值200元, 咱們能夠直接設置孫七爲VIP, 經過孫七的角色VIP就能夠從角色資源關係中找到對應的可操做的URL.
本文中的操做是基於[Shiro-認證]之上完成的, 建議先看完Shiro認證部分. Shiro的認證是經過內置的認證過濾器(authc)完成的, 同時也提供了一些受權相關的過濾器:
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部署新版本)
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訪問的用戶.
roles
用戶必須具備配置的角色才能夠訪問. Shiro會調用Realm
中查詢受權信息的方法獲取用戶的角色. 對應類爲org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
filterChainDefinitionMap = [
"/**" : "roles['admin,guest']" // 訪問用戶必須同時具有admin和guest角色才能夠訪問
]
複製代碼
如配置成roles["admin"]
表明只要是admin
角色就能夠訪問, 兩個及以上角色表明必須同時知足.
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)權限
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請求方式支持的不友好, 此過濾器應用較少.
上述內置過濾器中能夠支持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);
}
}
複製代碼
自定義的過濾器須要在Shiro中進行定義, 並配置URL須要受權才能訪問
// 配置自定義過濾器,名稱爲authz
authz(URLAuthorizationFilter) {
// 無權限頁面: 用戶無權限時重定向至該頁面
unauthorizedUrl = "/unauthorized.jsp"
}
複製代碼
// 配置URL規則
// 有請求訪問時Shiro會根據此規則找到對應的過濾器處理
filterChainDefinitionMap = [
"/unauthorized.jsp" : "anon", // 未受權頁不須要受權便可訪問
"/logout" : "logout", // 登出使用logout過濾器
"/login": "authc", // 登陸頁不配置受權
"/**": "authc, authz" // 其他全部頁面須要認證和受權(順序:先認證後受權)
]
複製代碼
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-認證中的模擬獲取用戶信息
}
}
複製代碼
啓動項目, 使用atd681
登陸後分別訪問/page/a
和/page/b
能夠正常訪問. 訪問/page/x
時重定向至未受權頁面
上述權限控制是當用戶訪問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沒有顯示.
至此, 基於Shiro受權的示例配置完成. 有興趣的同窗能夠多建立幾個用戶測試一下.
atd681-shiro-authz