寫在前面
經過前面的學習,我們了解了shiro中的認證流程,並且學會了如何通過自定義Realm實現應用程序的用戶認證。在這篇文章當中,我們將學習shiro中的授權流程。
授權概述
這里的授權指的是授予某一系統的某一用戶訪問受保護資源的權限,分為查詢、修改、插入和刪除幾類。沒有相關權限的用戶將無法訪問受保護資源,具有權限的用戶只能在自己權限范圍內操作受保護資源。
關鍵對象
主體(Subject)
即指定的某一用戶,這里的用戶可以是瀏覽器、APP和第三方應用程序等。
資源(Resource)
這里的資源包括資源本身和對資源的操作。資源本身具備資源類型和資源實例兩個屬性。用戶信息就是一個資源類型,向南的具體信息就是用戶信息的實例。資源操作主要由查詢、修改、刪除、添加等組成。
權限(Permission)
權限即是主體操作資源所需要的許可。權限本身不存在,只是某一系統的受保護資源的訪問標識。脫離了資源談權限就沒有了意義。
授權流程(訪問控制流程)
授權流程分析
前提:訪問主體已經通過認證,登錄到系統中。
-
用戶請求訪問某一受保護資源
-
判斷用戶是否具備訪問權限:
2.1 有,則執行步驟3
2.2 沒有,拒絕用戶訪問,並返回相應的提示
-
用戶訪問資源
授權模型
目前面向民用系統主流的授權模式主要有基於資源的的訪問控制和基於角色的訪問控制
-
基於角色的訪問控制RBAC (Role-Based Access Control)
其基本思想是,對系統操作的各種權限不是直接授予具體的用戶,而是在用戶集合與權限集合之間建立一個角色集合。每一種角色對應一組相應的權限。一旦用戶被分配了適當的角色后,該用戶就擁有此角色的所有操作權限。
-
基於資源的訪問控制RBAC(Resource-Based Access Control )
在基於角色的訪問控制當中,一但用戶的角色確定了,那么,其權限也就被固定下來了。也就是說具有相同角色的兩個主體,他們的權限是一樣的。沒有辦法做到動態地改變主體的權限。基於資源的訪問控制就是為了解決這個問題。
權限字符串
權限字符串由資源標識符、操作符、資源實例標識符和分隔符組成,格式:資源標識符:操作符:資源實例標識符
。其含義是:對指定的一個或多個資源實例具有指定的操作權限,當資源實例為任意個時,資源實例標識符用*
表示。分隔符:
也可以換成/
等形式。操作符一般分為create
、find
、update
、delete
四類,當具備該資源的所有操作時,操作符也可以用*
表示。當資源標識符、操作符、資源實例標識符都為*
時,代表該用戶具備該系統的所有資源訪問權限。
一些例子
- 對用戶01的查詢權限表示為:
user:find:01
- 對用戶具有查詢權限表示為:
user:find:*
或者user:find
- 對用戶01具有所有權限表示為:
user:*:01
上面只是列出了一種權限字符串的設計方法,大家在實踐當中可以根據自己的應用特點去設計自己的權限字符串。
shiro中的授權(訪問控制)實現方式
編程式
Subject currentSubject = SecurityUtils.getSubject();
if (currentSubject.hasRole("admin")){
//有權限的操作
}else{
//無權限
}
注解式(常用)
@RequiresRoles("admin")
public boolean createUser(User user){
//有權限才能執行方法
}
標簽式
//在jsp中
<shiro:hasRole name="admin">
//有權限才展示
</shiro:hasRole>
注意 :在Thymeleaf中使用時需要額外的集成。
編程實現
我們沿用上一篇文章當中的例子,來演示shiro授權部分的內容。文末附有本文例子的下載方式
基於角色的訪問控制
改造自定義Realm獲取角色信息
**自定義Realm對象
* @author 賴柄灃 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/10/4 11:00
*/
public class MySqlRealm extends AuthorizingRealm {
public MySqlRealm() {
//設置憑證匹配器,修改為hash憑證匹配器
HashedCredentialsMatcher myCredentialsMatcher = new HashedCredentialsMatcher();
//設置算法
myCredentialsMatcher.setHashAlgorithmName("md5");
//散列次數
myCredentialsMatcher.setHashIterations(1024);
this.setCredentialsMatcher(myCredentialsMatcher);
}
/**授權方法
* 對於授權方法,每次判斷主體是否具備對應權限時都會調用
* 因此,這里應當做緩存
* 緩存會在后面與springboot整合時講
* @author 賴柄灃 bingfengdev@aliyun.com
* @date 2020-10-04 11:01:50
* @param principalCollection
* @return org.apache.shiro.authz.AuthorizationInfo
* @throws AuthenticationException
* @version 1.0
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//1. 獲取當前主體的主身份信息,即用戶名
String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
//2. 根據主身份信息查詢數據庫,獲取主體具備的權限(模擬)
SimpleAuthorizationInfo authenticationInfo = null;
if ("xiangbei".equals(primaryPrincipal)){
authenticationInfo = new SimpleAuthorizationInfo();
authenticationInfo.addRole("admin");
}
return authenticationInfo;
}
/**認證
* @author 賴柄灃 bingfengdev@aliyun.com
* @date 2020-10-04 11:01:50
* @param authenticationToken
* @return org.apache.shiro.authz.AuthorizationInfo
* @throws AuthenticationException
* @version 1.0
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 1. 從token中獲取用戶名
String principal = (String) authenticationToken.getPrincipal();
//2. 根據用戶名查詢數據庫並封裝成authenticationinfo對象返回(模擬)
if (principal == "xiangbei") {
//四個參數分別是數據庫中的賬號、加密后的密碼、鹽值、realm名字
AuthenticationInfo authInfo = new SimpleAuthenticationInfo("xiangbei",
"ff595c47b51b4cf70fddce090f68879e",
ByteSource.Util.bytes("ee575f62-0dda-44f2-b75e-4efef795018f"),
this.getName());
return authInfo;
}
return null;
}
}
進行測試
hasRole
/**訪問控制測試
* @author 賴柄灃 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/10/5 16:48
*/
public class AuthzTest {
private CurrentSystemAuthenticator authenticator;
@Before
public void init() {
this.authenticator = new CurrentSystemAuthenticator();
//對於授權,只有主體通過認證后才能進行,所以需要先登錄系統
this.authenticator.authenticate("xiangbei","123");
}
@Test
public void testHasRole(){
Subject subject = SecurityUtils.getSubject();
//主體具備某一角色即可訪問
if (subject.hasRole("admin")){
System.out.println(subject.getPrincipal()+" 具有 admin 角色");
}
}
}
輸出
xiangbei 認證通過!
xiangbei 具有 admin 角色
hasRoles
**訪問控制測試
* @author 賴柄灃 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/10/5 16:48
*/
public class AuthzTest {
private CurrentSystemAuthenticator authenticator;
@Before
public void init() {
this.authenticator = new CurrentSystemAuthenticator();
//對於授權,只有主體通過認證后才能進行,所以需要先登錄系統
this.authenticator.authenticate("xiangbei","123");
}
/**
適用於只要有其中一個角色即可的情況
*/
@Test
public void testHasRoles(){
Subject subject = SecurityUtils.getSubject();
boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "user"));
for (boolean b : booleans) {
if (b) {
System.out.println(subject.getPrincipal()+" 具有訪問權限");
break;
}
}
}
}
輸出
xiangbei 認證通過!
xiangbei 具有訪問權限
hasAllRoles
/**訪問控制測試
* @author 賴柄灃 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/10/5 16:48
*/
public class AuthzTest {
private CurrentSystemAuthenticator authenticator;
@Before
public void init() {
this.authenticator = new CurrentSystemAuthenticator();
//對於授權,只有主體通過認證后才能進行,所以需要先登錄系統
this.authenticator.authenticate("xiangbei","123");
}
/**
具備所有角色才能訪問
*/
@Test
public void testHasAllRoles(){
Subject subject = SecurityUtils.getSubject();
boolean b = subject.hasAllRoles(Arrays.asList("admin", "user"));
if (b) {
System.out.println(subject.getPrincipal()+" 具有訪問權限");
}else {
System.out.println(subject.getPrincipal()+" 沒有訪問權限");
}
}
}
輸出
xiangbei 認證通過!
xiangbei 沒有訪問權限
基於資源的訪問控制
改造自定義Realm獲取資源權限信息
/**自定義Realm對象
* @author 賴柄灃 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/10/4 11:00
*/
public class MySqlRealm extends AuthorizingRealm {
public MySqlRealm() {
//設置憑證匹配器,修改為hash憑證匹配器
HashedCredentialsMatcher myCredentialsMatcher = new HashedCredentialsMatcher();
//設置算法
myCredentialsMatcher.setHashAlgorithmName("md5");
//散列次數
myCredentialsMatcher.setHashIterations(1024);
this.setCredentialsMatcher(myCredentialsMatcher);
}
/**授權方法
* 對於授權方法,每次判斷主體是否具備對應權限時都會調用
* 因此,這里應當做緩存
* 緩存會在后面與springboot整合時講
* @author 賴柄灃 bingfengdev@aliyun.com
* @date 2020-10-04 11:01:50
* @param principalCollection
* @return org.apache.shiro.authz.AuthorizationInfo
* @throws AuthenticationException
* @version 1.0
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//1. 獲取當前主體的主身份信息,即用戶名
String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
//2. 根據主身份信息查詢數據庫,獲取主體具備的權限(模擬)
SimpleAuthorizationInfo authenticationInfo = null;
if ("xiangbei".equals(primaryPrincipal)){
authenticationInfo = new SimpleAuthorizationInfo();
//authenticationInfo.addRole("admin");
//具備user的所有權限
authenticationInfo.addStringPermission("user:*");
//具備產品的創建權限
authenticationInfo.addStringPermission("product:create");
}
return authenticationInfo;
}
/**認證
* @author 賴柄灃 bingfengdev@aliyun.com
* @date 2020-10-04 11:01:50
* @param authenticationToken
* @return org.apache.shiro.authz.AuthorizationInfo
* @throws AuthenticationException
* @version 1.0
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 1. 從token中獲取用戶名
String principal = (String) authenticationToken.getPrincipal();
//2. 根據用戶名查詢數據庫並封裝成authenticationinfo對象返回(模擬)
if (principal == "xiangbei") {
//四個參數分別是數據庫中的賬號、加密后的密碼、鹽值、realm名字
AuthenticationInfo authInfo = new SimpleAuthenticationInfo("xiangbei",
"ff595c47b51b4cf70fddce090f68879e",
ByteSource.Util.bytes("ee575f62-0dda-44f2-b75e-4efef795018f"),
this.getName());
return authInfo;
}
return null;
}
}
進行測試
在上面的設定中,用戶xiangbei具有用戶資源的所有權限,對產品具有創建權限
isPermitted(String permission)
具備某一具體資源權限才能訪問
/**
* @author 賴柄灃 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/10/5 17:23
*/
public class AuthzTest2 {
private CurrentSystemAuthenticator authenticator;
@Before
public void init() {
this.authenticator = new CurrentSystemAuthenticator();
//對於授權,只有主體通過認證后才能進行,所以需要先登錄系統
this.authenticator.authenticate("xiangbei","123");
}
@Test
public void testIsPermission() {
Subject subject = SecurityUtils.getSubject();
if (subject.isPermitted("user:create")){
System.out.println(subject.getPrincipal() + " 具有 用戶創建權限");
}
}
}
輸出
xiangbei 認證通過!
xiangbei 具有 用戶創建權限
isPermitted(String... permission)或者isPermitted(List
permissions)
適用於要求具備某些權限的其中一部分的情況
/**
* @author 賴柄灃 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/10/5 17:23
*/
public class AuthzTest2 {
private CurrentSystemAuthenticator authenticator;
@Before
public void init() {
this.authenticator = new CurrentSystemAuthenticator();
//對於授權,只有主體通過認證后才能進行,所以需要先登錄系統
this.authenticator.authenticate("xiangbei","123");
}
@Test
public void testIsPermitted(){
Subject subject = SecurityUtils.getSubject();
boolean[] permitted = subject.isPermitted("user:create", "user:find");
for (boolean b : permitted) {
if (b) {
System.out.println(subject.getPrincipal() +" 具有訪問權限");
break;
}
}
}
}
isPermittedAll
這個適用於需要主體具備所列出的所有資源才能訪問的情況
/**
* @author 賴柄灃 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/10/5 17:23
*/
public class AuthzTest2 {
private CurrentSystemAuthenticator authenticator;
@Before
public void init() {
this.authenticator = new CurrentSystemAuthenticator();
//對於授權,只有主體通過認證后才能進行,所以需要先登錄系統
this.authenticator.authenticate("xiangbei","123");
}
@Test
public void testIsPermittedAll() {
Subject subject = SecurityUtils.getSubject();
boolean b = subject.isPermittedAll("product:*");
if (b) {
System.out.println(subject.getPrincipal() +" 具備 product 模塊的所有權限");
}else {
System.out.println(subject.getPrincipal() +" 沒有 product 模塊的訪問權限");
}
}
輸出
xiangbei 認證通過!
xiangbei 沒有 product 模塊的訪問權限
寫在后面
在這篇文章當中,我們主要學習了shrio的訪問控制,並通過一個簡單的例子給大家做了演示。在下一篇Shiro系列的文章當中,作者將介紹SpringBoot整合Shiro的相關內容。
如果您覺得這篇文章能給您帶來幫助,那么可以點贊鼓勵一下。如有錯誤之處,還請不吝賜教。在此,謝過各位鄉親父老!
本文例子下載方式: 微信搜索【Java開發實踐】,回復20201005 即可得到下載鏈接。