寫在前面
經過前面的學習,我們瞭解了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 即可得到下載連結。