Shiro框架授權(三)

本文轉載自跟我學Shiro

授權,也叫訪問控制,即在應用中控制誰能訪問哪些資源(如訪問頁面/編輯數據/頁面操作等)。在授權中需瞭解的幾個關鍵對象:主體(Subject)、資源(Resource)、權限(Permission)、角色(Role)。
主體:即訪問應用的用戶,在Shiro中使用Subject代表該用戶。用戶只有授權後才允許訪問相應的資源。
資源:在應用中用戶可以訪問的任何東西,比如訪問JSP頁面、查看/編輯某些數據、訪問某個業務方法、打印文本等等都是資源。用戶只要授權後才能訪問。
權限:安全策略中的原子授權單位,通過權限我們可以表示在應用中用戶有沒有操作某個資源的權力。即權限表示在應用中用戶能不能訪問某個資源,如:
訪問用戶列表頁面
查看/新增/修改/刪除用戶數據(即很多時候都是CRUD(增查改刪)式權限控制)
打印文檔等等。。。

如上可以看出,權限代表了用戶有沒有操作某個資源的權利,即反映在某個資源上的操作允不允許,不反映誰去執行這個操作。所以後續還需要把權限賦予給用戶,即定義哪個用戶允許在某個資源上做什麼操作(權限),Shiro不會去做這件事情,而是由實現人員提供。

Shiro支持粗粒度權限(如用戶模塊的所有權限)和細粒度權限(操作某個用戶的權限,即實例級別的),後續部分介紹。
角色:代表了操作集合,可以理解爲權限的集合,一般情況下我們會賦予用戶角色而不是權限,即這樣用戶可以擁有一組權限,賦予權限時比較方便。典型的如:項目經理、技術總監、CTO、開發工程師等都是角色,不同的角色擁有一組不同的權限。
隱式角色:即直接通過角色來驗證用戶有沒有操作權限,如在應用中CTO、技術總監、開發工程師可以使用打印機,假設某天不允許開發工程師使用打印機,此時需要從應用中刪除相應代碼;再如在應用中CTO、技術總監可以查看用戶、查看權限;突然有一天不允許技術總監查看用戶、查看權限了,需要在相關代碼中把技術總監角色從判斷邏輯中刪除掉;即粒度是以角色爲單位進行訪問控制的,粒度較粗;如果進行修改可能造成多處代碼修改。
顯示角色:在程序中通過權限控制誰能訪問某個資源,角色聚合一組權限集合;這樣假設哪個角色不能訪問某個資源,只需要從角色代表的權限集合中移除即可;無須修改多處代碼;即粒度是以資源/實例爲單位的;粒度較細。

一、授權方式

Shiro支持三種方式的授權:
編程式:通過寫if/else授權代碼塊完成:

Subject subject = SecurityUtils.getSubject();  
if(subject.hasRole(“admin”)) {  
    //有權限  
} else {  
    //無權限  
}   

註解式:通過在執行的Java方法上放置相應的註解完成:

@RequiresRoles("admin")  
public void hello() {  
    //有權限  
}   

沒有權限將拋出相應的異常;

JSP/GSP標籤:在JSP/GSP頁面通過相應的標籤完成:

<shiro:hasRole name="admin">  
<!— 有權限 —>  
</shiro:hasRole>

後續部分將詳細介紹如何使用。

二、授權

2.1 基於角色的訪問控制(隱式角色)

1.在ini配置文件配置用戶擁有的角色(shiro-role.ini)

[users]  
zhang=123,role1,role2  
wang=123,role1  

規則即:“用戶名=密碼,角色1,角色2”,如果需要在應用中判斷用戶是否有相應角色,就需要在相應的Realm中返回角色信息,也就是說Shiro不負責維護用戶-角色信息,需要應用提供,Shiro只是提供相應的接口方便驗證,後續會介紹如何動態的獲取用戶角色。

2.測試用例(com.github.crystal.chapter03.roleTest

@Test  
public void testHasRole() {  
    login("classpath:chapter03/shiro-role.ini", "crystal", "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/hasAllRoles用於判斷用戶是否擁有某個角色/某些權限;但是沒有提供如hashAnyRole用於判斷是否有某些權限中的某一個。

@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異常。

注意:如果很多地方進行了角色判斷,但是有一天不需要了那麼就需要修改相應代碼把所有相關的地方進行刪除;這就是粗粒度造成的問題。

2.2 基於資源的訪問控制(顯示角色)

1.在ini配置文件配置用戶擁有的角色及角色-權限關係(shiro-permission.ini)

[users]  
crystal=123,role1,role2  
barry=123,role1  
[roles]  
role1=user:create,user:update  
role2=user:create,user:delete 

規則:“用戶名=密碼,角色1,角色2”“角色=權限1,權限2”,即首先根據用戶名找到角色,然後根據角色再找到權限;即角色是權限集合;Shiro同樣不進行權限的維護,需要我們通過Realm返回相應的權限信息。只需要維護“用戶——角色”之間的關係即可。

2.測試用例(com.github.crystal.chapter03.PermissionTest

@Test  
public void testIsPermitted() {  
    login("classpath:chapter03/shiro-permission.ini", "crystal", "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用於判斷擁有某一個權限的接口。

@Test(expected = UnauthorizedException.class)  
public void testCheckPermission () {  
    login("classpath:chapter03/shiro-permission.ini", "crystal", "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異常。

到此基於資源的訪問控制(顯示角色)就完成了,也可以叫基於權限的訪問控制,這種方式的一般規則是“資源標識符:操作”,即是資源級別的粒度;這種方式的好處就是如果要修改基本都是一個資源級別的修改,不會對其他模塊代碼產生影響,粒度小。但是實現起來可能稍微複雜點,需要維護“用戶——角色,角色——權限(資源:操作)”之間的關係。

三、Permission

字符串通配符權限規則:“資源標識符:操作:對象實例ID” 即對哪個資源的哪個實例可以進行什麼操作。其默認支持通配符權限字符串,“:”表示資源/操作/實例的分割;“,”表示操作的分割;“*”表示任意資源/操作/實例。

3.1 單個資源單個權限

subject().checkPermissions("system:user:update"); 

用戶擁有資源“system:user”的“update”權限。

3.2 單個資源多個權限

ini配置文件

role41=system:user:update,system:user:delete  

然後通過如下代碼判斷

subject().checkPermissions("system:user:update", "system:user:delete");  

用戶擁有資源“system:user”的“update”和“delete”權限。如上可以簡寫成:
ini配置(表示角色4擁有system:user資源的update和delete權限)

role42="system:user:update,delete" 

接着可以通過如下代碼判斷

subject().checkPermissions("system:user:update,delete");  

通過“system:user:update,delete”驗證”system:user:update, system:user:delete”是沒問題的,但是反過來是規則不成立。

3.3 單個資源全部權限

ini配置

role51="system:user:create,update,delete,view"  

然後通過如下代碼判斷

subject().checkPermissions("system:user:create,delete,update:view");  

用戶擁有資源“system:user”的“create”、“update”、“delete”和“view”所有權限。如上可以簡寫成:

ini配置文件(表示角色5擁有system:user的所有權限)

role52=system:user:*  

也可以簡寫爲(推薦上邊的寫法):

role53=system:user  

然後通過如下代碼判斷

subject().checkPermissions("system:user:*");  
subject().checkPermissions("system:user"); 

通過“system:user:*”驗證“system:user:create,delete,update:view”可以,但是反過來是不成立的。

3.4 所有資源全部權限

ini配置

role61=*:view  

然後通過如下代碼判斷

subject().checkPermissions("user:view");  

用戶擁有所有資源的“view”所有權限。假設判斷的權限是“”system:user:view”,那麼需要“role5=::view”這樣寫纔行。

3.5 實例級別的權限

3.5.1 單個實例單個權限

ini配置

role71=user:view:1  

對資源user的1實例擁有view權限。
然後通過如下代碼判斷

subject().checkPermissions("user:view:1");  

3.5.2 單個實例多個權限

ini配置

role72="user:update,delete:1"  

對資源user的1實例擁有update、delete權限。
然後通過如下代碼判斷

subject().checkPermissions("user:delete,update:1");  
subject().checkPermissions("user:update:1", "user:delete:1"); 

3.5.3 單個實例所有權限

ini配置

role73=user:*:1  

對資源user的1實例擁有所有權限。
然後通過如下代碼判斷

subject().checkPermissions("user:update:1", "user:delete:1", "user:view:1");

3.5.4 所有實例單個權限

ini配置

role74=user:auth:*  

對資源user的1實例擁有所有權限。
然後通過如下代碼判斷

subject().checkPermissions("user:auth:1", "user:auth:2");  

3.5.5 所有實例所有權限

ini配置

role75=user:*:* 

對資源user的1實例擁有所有權限。
然後通過如下代碼判斷

subject().checkPermissions("user:view:1", "user:auth:2");  

3.5.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”,即後綴匹配必須指定前綴(多個冒號就需要多個來匹配)。

3.5.7 WildcardPermission

如下兩種方式是等價的:

subject().checkPermission("menu:view:1");  
subject().checkPermission(new WildcardPermission("menu:view:1"));   

因此沒什麼必要的話使用字符串更方便。

3.5.8 性能問題

通配符匹配方式比字符串相等匹配來說是更復雜的,因此需要花費更長時間,但是一般系統的權限不會太多,且可以配合緩存來提供其性能,如果這樣性能還達不到要求我們可以實現位操作算法實現性能更好的權限匹配。另外實例級別的權限驗證如果數據量太大也不建議使用,可能造成查詢權限及匹配變慢。可以考慮比如在sql查詢時加上權限字符串之類的方式在查詢時就完成了權限匹配。

四、授權流程

這裏寫圖片描述
流程如下:
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匹配流程:
1、首先檢查相應的Realm是否實現了實現了Authorizer;
2、如果實現了Authorizer,那麼接着調用其相應的isPermitted*/hasRole*接口進行匹配;
3、如果有一個Realm匹配那麼將返回true,否則返回false。

如果Realm進行授權的話,應該繼承AuthorizingRealm,其流程是:
1、如果調用hasRole*,則直接獲取AuthorizationInfo.getRoles()與傳入的角色比較即可;
2、首先如果調用如isPermitted(“user:view”),首先通過PermissionResolver將權限字符串轉換成相應的Permission實例,默認使用WildcardPermissionResolver,即轉換爲通配符的WildcardPermission;
3、通過AuthorizationInfo.getObjectPermissions()得到Permission實例集合;通過AuthorizationInfo. getStringPermissions()得到字符串集合並通過PermissionResolver解析爲Permission實例;然後獲取用戶的角色,並通過RolePermissionResolver解析角色對應的權限集合(默認沒有實現,可以自己提供);
4、接着調用Permission. implies(Permission p)逐個與傳入的權限比較,如果有匹配的則返回true,否則false。

五、Authorizer、PermissionResolver及RolePermissionResolver

Authorizer的職責是進行授權(訪問控制),是Shiro API中授權核心的入口點,其提供了相應的角色/權限判斷接口,具體請參考其Javadoc。SecurityManager繼承了Authorizer接口,且提供了ModularRealmAuthorizer用於多Realm時的授權匹配。PermissionResolver用於解析權限字符串到Permission實例,而RolePermissionResolver用於根據角色解析相應的權限集合。我們可以

通過如下ini配置更改Authorizer實現:

authorizer=org.apache.shiro.authz.ModularRealmAuthorizer  
securityManager.authorizer=$authorizer   

對於ModularRealmAuthorizer,相應的AuthorizingSecurityManager會在初始化完成後自動將相應的realm設置進去,我們也可以通過調用其setRealms()方法進行設置。對於實現自己的authorizer可以參考ModularRealmAuthorizer實現即可,在此就不提供示例了。

設置ModularRealmAuthorizer的permissionResolver,其會自動設置到相應的Realm上(其實現了PermissionResolverAware接口),如:

permissionResolver=com.github.crystal.chapter03.permission.BitAndWildPermissionResolver  
authorizer.permissionResolver=$permissionResolver   

設置ModularRealmAuthorizer的rolePermissionResolver,其會自動設置到相應的Realm上(其實現了RolePermissionResolverAware接口),如:

rolePermissionResolver=com.github.crystal.chapter03.permission.MyRolePermissionResolver  
authorizer.rolePermissionResolver=$rolePermissionResolver   

例子

1、ini配置(shiro-authorizer.ini)

[main]  
#自定義authorizer  
authorizer=org.apache.shiro.authz.ModularRealmAuthorizer  
#自定義permissionResolver  
#permissionResolver=org.apache.shiro.authz.permission.WildcardPermissionResolver  
permissionResolver=com.github.crystal.chapter03.permission.BitAndWildPermissionResolver  
authorizer.permissionResolver=$permissionResolver  
#自定義rolePermissionResolver  
rolePermissionResolver=com.github.crystal.chapter03.permission.MyRolePermissionResolver  
authorizer.rolePermissionResolver=$rolePermissionResolver  

securityManager.authorizer=$authorizer  

#自定義realm 一定要放在securityManager.authorizer賦值之後(因爲調用setRealms會將realms設置給authorizer,並給各個Realm設置permissionResolver和rolePermissionResolver)  
realm=com.github.crystal.chapter03.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擁有修改/查看權限。

public class BitPermission implements Permission {

    private String resourceIdentify;//資源  
    private int permissionBit;  //權限
    private String instanceId; //實例id

    public BitPermission(String permissionStr) {
        String[] arr = permissionStr.split("\\+");
        if (arr.length > 1) {
            resourceIdentify = arr[1];
        }
        if(StringUtils.isEmpty(resourceIdentify)) {  
            resourceIdentify = "*";  
        }  
        if (arr.length > 2) {
            permissionBit = Integer.parseInt(arr[2]);
        }
        if(arr.length > 3) {  
            instanceId = arr[3];  
        }  
        if(StringUtils.isEmpty(instanceId)) {  
            instanceId = "*";  
        }  
    }

    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)方法用於判斷權限匹配的;

public class BitAndWildPermissionResolver implements PermissionResolver {

    public Permission resolvePermission(String permissionString) {
        if(permissionString.startsWith("+")) {  
            return new BitPermission(permissionString);  
        }  
        return new WildcardPermission(permissionString);  
    }
}

}
BitAndWildPermissionResolver實現了PermissionResolver接口,並根據權限字符串是否以“+”開頭來解析權限字符串爲BitPermission或WildcardPermission。

3、定義MyRolePermissionResolver

RolePermissionResolver用於根據角色字符串來解析得到權限集合。

public class MyRolePermissionResolver implements RolePermissionResolver {

    public Collection<Permission> resolvePermissionsInRole(String roleString) {
        if("role1".equals(roleString)) {  
            return Arrays.asList((Permission)new WildcardPermission("menu:*"));  
        }  
        return null;  
    }
}

此處的實現很簡單,如果用戶擁有role1,那麼就返回一個“menu:*”的權限。

4、自定義Realm

ublic 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 {
        String username = (String)token.getPrincipal();  //得到用戶名
        String password = new String((char[])token.getCredentials()); //得到密碼
        if(!"crystal".equals(username)) {
            throw new UnknownAccountException(); //如果用戶名錯誤
        }
        if(!"123".equals(password)) {
            throw new IncorrectCredentialsException(); //如果密碼錯誤
        }
        //如果身份認證驗證成功,返回一個AuthenticationInfo實現;
        return new SimpleAuthenticationInfo(username, password, getName());
    }
}

此時我們繼承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、測試用例

@Test  
    public void testIsPermitted() {  
        login("classpath:chapter03/shiro-authorizer.ini", "crystal", "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裏的驗證邏輯實現的。
示例代碼:shiro-learn

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章