## 授權
授權,也叫訪問控制,即在應用中控制誰能訪問哪些資源(如訪問頁面/編輯數據/頁面操作等)。在授權中需了解的幾個關鍵對象:主體(Subject)、資源(Resource)、權限(Permission)、角色(Role)。
**主體**
主體,即訪問應用的用戶,在 Shiro 中使用 Subject 代表該用戶。用戶只有授權后才允許訪問相應的資源。
**資源**
在應用中用戶可以訪問的任何東西,比如訪問 JSP 頁面、查看/編輯某些數據、訪問某個業務方法、打印文本等等都是資源。用戶只要授權后才能訪問。
**權限**
安全策略中的原子授權單位,通過權限我們可以表示在應用中用戶有沒有操作某個資源的權力。即權限表示在應用中用戶能不能訪問某個資源,如: 訪問用戶列表頁面
查看/新增/修改/刪除用戶數據(即很多時候都是 CRUD(增查改刪)式權限控制)
打印文檔等等。。。
如上可以看出,權限代表了用戶有沒有操作某個資源的權利,即反映在某個資源上的操作允不允許,不反映誰去執行這個操作。所以后續還需要把權限賦予給用戶,即定義哪個用戶允許在某個資源上做什么操作(權限),Shiro 不會去做這件事情,而是由實現人員提供。
Shiro 支持粗粒度權限(如用戶模塊的所有權限)和細粒度權限(操作某個用戶的權限,即實例級別的),后續部分介紹。
**角色**
角色代表了操作集合,可以理解為權限的集合,一般情況下我們會賦予用戶角色而不是權限,即這樣用戶可以擁有一組權限,賦予權限時比較方便。典型的如:項目經理、技術總監、CTO、開發工程師等都是角色,不同的角色擁有一組不同的權限。
**隱式角色**:
即直接通過角色來驗證用戶有沒有操作權限,如在應用中 CTO、技術總監、開發工程師可以使用打印機,假設某天不允許開發工程師使用打印機,此時需要從應用中刪除相應代碼;再如在應用中 CTO、技術總監可以查看用戶、查看權限;突然有一天不允許技術總監查看用戶、查看權限了,需要在相關代碼中把技術總監角色從判斷邏輯中刪除掉;即粒度是以角色為單位進行訪問控制的,粒度較粗;如果進行修改可能造成多處代碼修改。
**顯示角色**:
在程序中通過權限控制誰能訪問某個資源,角色聚合一組權限集合;這樣假設哪個角色不能訪問某個資源,只需要從角色代表的權限集合中移除即可;無須修改多處代碼;即粒度是以資源/實例為單位的;粒度較細。
請 google 搜索“RBAC”和“RBAC新解”分別了解“基于角色的訪問控制”“基于資源的訪問控制(Resource-Based Access Control)”。
## 3.1 授權方式
Shiro 支持三種方式的授權:
編程式:通過寫 if/else 授權代碼塊完成:
``` java
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
//有權限
} else {
//無權限
}
```
注解式:通過在執行的 Java 方法上放置相應的注解完成:
``` java
@RequiresRoles("admin")
public void hello() {
//有權限
}
```
沒有權限將拋出相應的異常;
JSP/GSP 標簽:在 JSP/GSP 頁面通過相應的標簽完成:
``` xml
<shiro:hasRole name="admin">
<!— 有權限 —>
</shiro:hasRole>
```
后續部分將詳細介紹如何使用。
## 3.2 授權
**基于角色的訪問控制(隱式角色)**
1、在 ini 配置文件配置用戶擁有的角色(shiro-role.ini)
``` ini
[users]
zhang=123,role1,role2
wang=123,role1
```
規則即:“用戶名=密碼,角色1,角色2”,如果需要在應用中判斷用戶是否有相應角色,就需要在相應的 Realm 中返回角色信息,也就是說 Shiro 不負責維護用戶-角色信息,需要應用提供,Shiro 只是提供相應的接口方便驗證,后續會介紹如何動態的獲取用戶角色。
2、測試用例(com.github.zhangkaitao.shiro.chapter3.RoleTest)
``` java
@Test
public void testHasRole() {
login("classpath:shiro-role.ini", "zhang", "123");
//判斷擁有角色:role1
Assert.assertTrue(subject().hasRole("role1"));
//判斷擁有角色:role1 and role2
Assert.assertTrue(subject().hasAllRoles(Arrays.asList("role1", "role2")));
//判斷擁有角色:role1 and role2 and !role3
boolean[] result = subject().hasRoles(Arrays.asList("role1", "role2", "role3"));
Assert.assertEquals(true, result[0]);
Assert.assertEquals(true, result[1]);
Assert.assertEquals(false, result[2]);
}
```
Shiro 提供了 hasRole/hasRole 用于判斷用戶是否擁有某個角色/某些權限;但是沒有提供如 hashAnyRole 用于判斷是否有某些權限中的某一個。
``` java
@Test(expected = UnauthorizedException.class)
public void testCheckRole() {
login("classpath:shiro-role.ini", "zhang", "123");
//斷言擁有角色:role1
subject().checkRole("role1");
//斷言擁有角色:role1 and role3 失敗拋出異常
subject().checkRoles("role1", "role3");
}
```
Shiro 提供的 checkRole/checkRoles 和 hasRole/hasAllRoles 不同的地方是它在判斷為假的情況下會拋出 UnauthorizedException 異常。
到此基于角色的訪問控制(即隱式角色)就完成了,這種方式的缺點就是如果很多地方進行了角色判斷,但是有一天不需要了那么就需要修改相應代碼把所有相關的地方進行刪除;這就是粗粒度造成的問題。
**基于資源的訪問控制(顯示角色)**
1、在 ini 配置文件配置用戶擁有的角色及角色-權限關系(shiro-permission.ini)
``` ini
[users]
zhang=123,role1,role2
wang=123,role1
[roles]
role1=user:create,user:update
role2=user:create,user:delete
```
規則:“用戶名=密碼,角色 1,角色 2”“角色=權限 1,權限 2”,即首先根據用戶名找到角色,然后根據角色再找到權限;即角色是權限集合;Shiro 同樣不進行權限的維護,需要我們通過 Realm 返回相應的權限信息。只需要維護“用戶——角色”之間的關系即可。
2、測試用例(com.github.zhangkaitao.shiro.chapter3.PermissionTest)
``` java
@Test
public void testIsPermitted() {
login("classpath:shiro-permission.ini", "zhang", "123");
//判斷擁有權限:user:create
Assert.assertTrue(subject().isPermitted("user:create"));
//判斷擁有權限:user:update and user:delete
Assert.assertTrue(subject().isPermittedAll("user:update", "user:delete"));
//判斷沒有權限:user:view
Assert.assertFalse(subject().isPermitted("user:view"));
}
```
Shiro 提供了 isPermitted 和 isPermittedAll 用于判斷用戶是否擁有某個權限或所有權限,也沒有提供如 isPermittedAny 用于判斷擁有某一個權限的接口。
``` java
@Test(expected = UnauthorizedException.class)
public void testCheckPermission () {
login("classpath:shiro-permission.ini", "zhang", "123");
//斷言擁有權限:user:create
subject().checkPermission("user:create");
//斷言擁有權限:user:delete and user:update
subject().checkPermissions("user:delete", "user:update");
//斷言擁有權限:user:view 失敗拋出異常
subject().checkPermissions("user:view");
}
```
但是失敗的情況下會拋出 UnauthorizedException 異常。
到此基于資源的訪問控制(顯示角色)就完成了,也可以叫基于權限的訪問控制,這種方式的一般規則是“資源標識符:操作”,即是資源級別的粒度;這種方式的好處就是如果要修改基本都是一個資源級別的修改,不會對其他模塊代碼產生影響,粒度小。但是實現起來可能稍微復雜點,需要維護“用戶——角色,角色——權限(資源:操作)”之間的關系。
## 3.3 Permission
### 字符串通配符權限
規則:“資源標識符:操作:對象實例 ID” 即對哪個資源的哪個實例可以進行什么操作。其默認支持通配符權限字符串,“:”表示資源/操作/實例的分割;“,”表示操作的分割;“\*”表示任意資源/操作/實例。
**1、單個資源單個權限**
``` java
subject().checkPermissions("system:user:update");
````
用戶擁有資源“system:user”的“update”權限。
**2、單個資源多個權限**
``` ini
role41=system:user:update,system:user:delete
```
然后通過如下代碼判斷
``` java
subject().checkPermissions("system:user:update", "system:user:delete");
```
用戶擁有資源“system:user”的“update”和“delete”權限。如上可以簡寫成:
ini 配置(表示角色4擁有 system:user 資源的 update 和 delete 權限)
``` ini
role42="system:user:update,delete"
```
接著可以通過如下代碼判斷
``` java
subject().checkPermissions("system:user:update,delete");
```
通過“system:user:update,delete”驗證“system:user:update, system:user:delete”是沒問題的,但是反過來是規則不成立。
**3、單個資源全部權限**
ini 配置
``` ini
role51="system:user:create,update,delete,view"
```
然后通過如下代碼判斷
``` java
subject().checkPermissions("system:user:create,delete,update:view");
```
用戶擁有資源“system:user”的“create”、“update”、“delete”和“view”所有權限。如上可以簡寫成:
ini 配置文件(表示角色 5 擁有 system:user 的所有權限)
``` ini
role52=system:user:*
```
也可以簡寫為(推薦上邊的寫法):
``` ini
role53=system:user
```
然后通過如下代碼判斷
``` java
subject().checkPermissions("system:user:*");
subject().checkPermissions("system:user");
```
通過“system:user:\*”驗證“system:user:create,delete,update:view”可以,但是反過來是不成立的。
**4、所有資源全部權限**
ini 配置
``` ini
role61=*:view
```
然后通過如下代碼判斷
``` java
subject().checkPermissions("user:view");
```
用戶擁有所有資源的“view”所有權限。假設判斷的權限是“"system:user:view”,那么需要“role5=*:*:view”這樣寫才行
**5、實例級別的權限**
* 單個實例單個權限
ini 配置
``` ini
role71=user:view:1
```
對資源 user 的 1 實例擁有 view 權限。
然后通過如下代碼判斷
```
subject().checkPermissions("user:view:1");
```
* 單個實例多個權限
ini 配置
``` ini
role72="user:update,delete:1"
```
對資源 user 的 1 實例擁有 update、delete 權限。
然后通過如下代碼判斷
```
subject().checkPermissions("user:delete,update:1");
subject().checkPermissions("user:update:1", "user:delete:1");
```
* 單個實例所有權限
ini 配置
``` ini
role73=user:*:1
```
對資源 user 的 1 實例擁有所有權限。
然后通過如下代碼判斷
``` java
subject().checkPermissions("user:update:1", "user:delete:1", "user:view:1");
```
* 所有實例單個權限
ini 配置
``` ini
role74=user:auth:*
```
對資源 user 的 1 實例擁有所有權限。
然后通過如下代碼判斷
``` java
subject().checkPermissions("user:auth:1", "user:auth:2");
```
* 所有實例所有權限
ini 配置
``` ini
role75=user:*:*
```
對資源 user 的 1 實例擁有所有權限。
然后通過如下代碼判斷
``` java
subject().checkPermissions("user:view:1", "user:auth:2");
```
**6、Shiro 對權限字符串缺失部分的處理**
如“user:view”等價于“`user:view:*`”;而“organization”等價于“`organization:*`”或者“`organization:*:*`”。可以這么理解,這種方式實現了前綴匹配。
另外如“`user:*`”可以匹配如“`user:delete`”、“`user:delete`”可以匹配如“`user:delete:1`”、“`user:*:1`”可以匹配如“user:view:1”、“user”可以匹配“`user:view`”或“`user:view:1`”等。即`*`可以匹配所有,不加`*`可以進行前綴匹配;但是如“`*:view`”不能匹配“`system:user:view`”,需要使用“`*:*:view`”,即后綴匹配必須指定前綴(多個冒號就需要多個`*`來匹配)。
**7、WildcardPermission**
如下兩種方式是等價的:
``` java
subject().checkPermission("menu:view:1");
subject().checkPermission(new WildcardPermission("menu:view:1"));
```
因此沒什么必要的話使用字符串更方便。
**8、性能問題**
通配符匹配方式比字符串相等匹配來說是更復雜的,因此需要花費更長時間,但是一般系統的權限不會太多,且可以配合緩存來提供其性能,如果這樣性能還達不到要求我們可以實現位操作算法實現性能更好的權限匹配。另外實例級別的權限驗證如果數據量太大也不建議使用,可能造成查詢權限及匹配變慢。可以考慮比如在sql查詢時加上權限字符串之類的方式在查詢時就完成了權限匹配。
## 3.4 授權流程

流程如下:
1. 首先調用 `Subject.isPermitted*/hasRole*`接口,其會委托給 SecurityManager,而 SecurityManager 接著會委托給 Authorizer;
2. Authorizer 是真正的授權者,如果我們調用如 isPermitted(“user:view”),其首先會通過 PermissionResolver 把字符串轉換成相應的 Permission 實例;
3. 在進行授權之前,其會調用相應的 Realm 獲取 Subject 相應的角色/權限用于匹配傳入的角色/權限;
4. Authorizer 會判斷 Realm 的角色/權限是否和傳入的匹配,如果有多個 Realm,會委托給 ModularRealmAuthorizer 進行循環判斷,如果匹配如 `isPermitted*/hasRole*` 會返回 true,否則返回 false 表示授權失敗。
ModularRealmAuthorizer 進行多 Realm 匹配流程:
* 首先檢查相應的 Realm 是否實現了實現了 Authorizer;
* 如果實現了 Authorizer,那么接著調用其相應的 `isPermitted*/hasRole*` 接口進行匹配;
* 如果有一個 Realm 匹配那么將返回 true,否則返回 false。
如果 Realm 進行授權的話,應該繼承 AuthorizingRealm,其流程是:
* 如果調用 `hasRole*`,則直接獲取 AuthorizationInfo.getRoles() 與傳入的角色比較即可;首先如果調用如 isPermitted(“user:view”),首先通過 PermissionResolver 將權限字符串轉換成相應的 Permission 實例,默認使用 WildcardPermissionResolver,即轉換為通配符的 WildcardPermission;
* 通過 AuthorizationInfo.getObjectPermissions() 得到 Permission 實例集合;通過 AuthorizationInfo.getStringPermissions() 得到字符串集合并通過 PermissionResolver 解析為 Permission 實例;然后獲取用戶的角色,并通過 RolePermissionResolver 解析角色對應的權限集合(默認沒有實現,可以自己提供);
* 接著調用 Permission.implies(Permission p) 逐個與傳入的權限比較,如果有匹配的則返回 true,否則 false。
## 3.5 Authorizer、PermissionResolver及RolePermissionResolver
Authorizer 的職責是進行授權(訪問控制),是 Shiro API 中授權核心的入口點,其提供了相應的角色/權限判斷接口,具體請參考其 Javadoc。SecurityManager 繼承了 Authorizer 接口,且提供了 ModularRealmAuthorizer 用于多 Realm 時的授權匹配。PermissionResolver 用于解析權限字符串到 Permission 實例,而 RolePermissionResolver 用于根據角色解析相應的權限集合。
我們可以通過如下 ini 配置更改 Authorizer 實現:
``` ini
authorizer=org.apache.shiro.authz.ModularRealmAuthorizer
securityManager.authorizer=$authorizer
```
對于 ModularRealmAuthorizer,相應的 AuthorizingSecurityManager 會在初始化完成后自動將相應的 realm 設置進去,我們也可以通過調用其 setRealms() 方法進行設置。對于實現自己的 authorizer 可以參考 ModularRealmAuthorizer 實現即可,在此就不提供示例了。
設置 ModularRealmAuthorizer 的 permissionResolver,其會自動設置到相應的 Realm 上(其實現了 PermissionResolverAware 接口),如:
``` java
permissionResolver=org.apache.shiro.authz.permission.WildcardPermissionResolver
authorizer.permissionResolver=$permissionResolver
```
設置 ModularRealmAuthorizer 的 rolePermissionResolver,其會自動設置到相應的 Realm 上(其實現了 RolePermissionResolverAware 接口),如:
``` ini
rolePermissionResolver=com.github.zhangkaitao.shiro.chapter3.permission.MyRolePermissionResolver
authorizer.rolePermissionResolver=$rolePermissionResolver
```
**示例**
**1、ini 配置(shiro-authorizer.ini)**
``` ini
[main]
#自定義authorizer
authorizer=org.apache.shiro.authz.ModularRealmAuthorizer
#自定義permissionResolver
#permissionResolver=org.apache.shiro.authz.permission.WildcardPermissionResolver
permissionResolver=com.github.zhangkaitao.shiro.chapter3.permission.BitAndWildPermissionResolver
authorizer.permissionResolver=$permissionResolver
#自定義rolePermissionResolver
rolePermissionResolver=com.github.zhangkaitao.shiro.chapter3.permission.MyRolePermissionResolver
authorizer.rolePermissionResolver=$rolePermissionResolver
securityManager.authorizer=$authorizer
```
``` ini
#自定義realm 一定要放在securityManager.authorizer賦值之后(因為調用setRealms會將realms設置給authorizer,并給各個Realm設置permissionResolver和rolePermissionResolver)
realm=com.github.zhangkaitao.shiro.chapter3.realm.MyRealm
securityManager.realms=$realm
```
設置 securityManager 的 realms 一定要放到最后,因為在調用 SecurityManager.setRealms 時會將 realms 設置給 authorizer,并為各個 Realm 設置 permissionResolver 和 rolePermissionResolver。另外,不能使用 IniSecurityManagerFactory 創建的 IniRealm,因為其初始化順序的問題可能造成后續的初始化 Permission 造成影響。
**2、定義 BitAndWildPermissionResolver 及 BitPermission**
BitPermission 用于實現位移方式的權限,如規則是:
權限字符串格式:+ 資源字符串 + 權限位 + 實例 ID;以 + 開頭中間通過 + 分割;權限:0 表示所有權限;1 新增(二進制:0001)、2 修改(二進制:0010)、4 刪除(二進制:0100)、8 查看(二進制:1000);如 +user+10 表示對資源 user 擁有修改 / 查看權限。
``` java
public class BitPermission implements Permission {
private String resourceIdentify;
private int permissionBit;
private String instanceId;
public BitPermission(String permissionString) {
String[] array = permissionString.split("\\+");
if(array.length > 1) {
resourceIdentify = array[1];
}
if(StringUtils.isEmpty(resourceIdentify)) {
resourceIdentify = "*";
}
if(array.length > 2) {
permissionBit = Integer.valueOf(array[2]);
}
if(array.length > 3) {
instanceId = array[3];
}
if(StringUtils.isEmpty(instanceId)) {
instanceId = "*";
}
}
@Override
public boolean implies(Permission p) {
if(!(p instanceof BitPermission)) {
return false;
}
BitPermission other = (BitPermission) p;
if(!("*".equals(this.resourceIdentify) || this.resourceIdentify.equals(other.resourceIdentify))) {
return false;
}
if(!(this.permissionBit ==0 || (this.permissionBit & other.permissionBit) != 0)) {
return false;
}
if(!("*".equals(this.instanceId) || this.instanceId.equals(other.instanceId))) {
return false;
}
return true;
}
}
```
Permission 接口提供了 boolean implies(Permission p) 方法用于判斷權限匹配的;
``` java
public class BitAndWildPermissionResolver implements PermissionResolver {
@Override
public Permission resolvePermission(String permissionString) {
if(permissionString.startsWith("+")) {
return new BitPermission(permissionString);
}
return new WildcardPermission(permissionString);
}
}
```
BitAndWildPermissionResolver 實現了 PermissionResolver 接口,并根據權限字符串是否以 “+” 開頭來解析權限字符串為 BitPermission 或 WildcardPermission。
**3、定義 MyRolePermissionResolver**
RolePermissionResolver 用于根據角色字符串來解析得到權限集合。
``` java
public class MyRolePermissionResolver implements RolePermissionResolver {
@Override
public Collection<Permission> resolvePermissionsInRole(String roleString) {
if("role1".equals(roleString)) {
return Arrays.asList((Permission)new WildcardPermission("menu:*"));
}
return null;
}
}
```
此處的實現很簡單,如果用戶擁有 role1,那么就返回一個 “`menu:*`” 的權限。
**4、自定義 Realm**
``` java
public class MyRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addRole("role1");
authorizationInfo.addRole("role2");
authorizationInfo.addObjectPermission(new BitPermission("+user1+10"));
authorizationInfo.addObjectPermission(new WildcardPermission("user1:*"));
authorizationInfo.addStringPermission("+user2+10");
authorizationInfo.addStringPermission("user2:*");
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//和com.github.zhangkaitao.shiro.chapter2.realm.MyRealm1. getAuthenticationInfo代碼一樣,省略
}
}
```
此時我們繼承 AuthorizingRealm 而不是實現 Realm 接口;推薦使用 AuthorizingRealm,因為: AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token):表示獲取身份驗證信息;AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals):表示根據用戶身份獲取授權信息。這種方式的好處是當只需要身份驗證時只需要獲取身份驗證信息而不需要獲取授權信息。對于 AuthenticationInfo 和 AuthorizationInfo 請參考其 Javadoc 獲取相關接口信息。
另外我們可以使用 JdbcRealm,需要做的操作如下:
1. 執行 sql/ shiro-init-data.sql 插入相關的權限數據;
2. 使用 shiro-jdbc-authorizer.ini 配置文件,需要設置 jdbcRealm.permissionsLookupEnabled 為 true 來開啟權限查詢。
此次還要注意就是不能把我們自定義的如 “+user1+10” 配置到 INI 配置文件,即使有 IniRealm 完成,因為 IniRealm 在 new 完成后就會解析這些權限字符串,默認使用了WildcardPermissionResolver 完成,即此處是一個設計權限,如果采用生命周期(如使用初始化方法)的方式進行加載就可以解決我們自定義 permissionResolver 的問題。
**5、測試用例**
``` java
public class AuthorizerTest extends BaseTest {
@Test
public void testIsPermitted() {
login("classpath:shiro-authorizer.ini", "zhang", "123");
//判斷擁有權限:user:create
Assert.assertTrue(subject().isPermitted("user1:update"));
Assert.assertTrue(subject().isPermitted("user2:update"));
//通過二進制位的方式表示權限
Assert.assertTrue(subject().isPermitted("+user1+2"));//新增權限
Assert.assertTrue(subject().isPermitted("+user1+8"));//查看權限
Assert.assertTrue(subject().isPermitted("+user2+10"));//新增及查看
Assert.assertFalse(subject().isPermitted("+user1+4"));//沒有刪除權限
Assert.assertTrue(subject().isPermitted("menu:view"));//通過MyRolePermissionResolver解析得到的權限
}
}
```
通過如上步驟可以實現自定義權限驗證了。另外因為不支持 hasAnyRole/isPermittedAny 這種方式的授權,可以參考我的一篇《簡單 shiro 擴展實現 NOT、AND、OR 權限驗證 》進行簡單的擴展完成這個需求,在這篇文章中通過重寫 AuthorizingRealm 里的驗證邏輯實現的。
- 第1章 Shiro簡介
- 1.1 簡介
- 第2章 身份驗證
- 第3章 授權
- 第4章 INI配置
- 第5章 編碼 / 加密
- 第6章 Realm及相關對象
- 第7章 與Web集成
- 第8章 攔截器機制
- 第9章 JSP標簽
- 第10章 會話管理
- 第11章 緩存機制
- 第12章 與Spring集成
- 第13章 RememberMe
- 第14章 SSL
- 第15章 單點登錄
- 第16章 綜合實例
- 第17章 OAuth2集成
- 第18章 并發登錄人數控制
- 第19章 動態URL權限控制
- 第20章 無狀態Web應用集成
- 第21章 授予身份及切換身份
- 第22章 集成驗證碼
- 第23章 多項目集中權限管理及分布式會話
- 第24章 在線會話管理