shiro之受權

受權,也叫訪問控制,即在應用中控制誰能訪問哪些資源(如訪問頁面/編輯數據/頁面操做等)。在受權中需瞭解的幾個關鍵對象:主體(Subject)、資源(Resource)、權限(Permission)、角色(Role)。git

主體
主體,即訪問應用的用戶,在Shiro中使用Subject表明該用戶。用戶只有受權後才容許訪問相應的資源。github

資源
在應用中用戶能夠訪問的任何東西,好比訪問JSP頁面、查看/編輯某些數據、訪問某個業務方法、打印文本等等都是資源。用戶只要受權後才能訪問。sql

權限
安全策略中的原子受權單位,經過權限咱們能夠表示在應用中用戶有沒有操做某個資源的權力。即權限表示在應用中用戶能不能訪問某個資源,如:
訪問用戶列表頁面
查看/新增/修改/刪除用戶數據(即不少時候都是CRUD(增查改刪)式權限控制)
打印文檔等等。。。apache

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

Shiro支持粗粒度權限(如用戶模塊的全部權限)和細粒度權限(操做某個用戶的權限,即實例級別的)安全

角色
角色表明了操做集合,能夠理解爲權限的集合,通常狀況下咱們會賦予用戶角色而不是權限,即這樣用戶能夠擁有一組權限,賦予權限時比較方便。典型的如:項目經理、技術總監、CTO、開發工程師等都是角色,不一樣的角色擁有一組不一樣的權限。ide

隱式角色:即直接經過角色來驗證用戶有沒有操做權限,如在應用中CTO、技術總監、開發工程師可使用打印機,假設某天不容許開發工程師使用打印機,此時須要從應用中刪除相應代碼;再如在應用中CTO、技術總監能夠查看用戶、查看權限;忽然有一天不容許技術總監查看用戶、查看權限了,須要在相關代碼中把技術總監角色從判斷邏輯中刪除掉;即粒度是以角色爲單位進行訪問控制的,粒度較粗;若是進行修改可能形成多處代碼修改。測試

顯示角色:在程序中經過權限控制誰能訪問某個資源,角色聚合一組權限集合;這樣假設哪一個角色不能訪問某個資源,只須要從角色表明的權限集合中移除便可;無須修改多處代碼;即粒度是以資源/實例爲單位的;粒度較細。ui

受權方式this

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 >

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

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

[users]
zhang=123,role1,role2
wang=123,role1
規則即:「用戶名=密碼,角色1,角色2」,若是須要在應用中判斷用戶是否有相應角色,就須要在相應的Realm中返回角色信息,也就是說Shiro不負責維護用戶-角色信息,須要應用提供,Shiro只是提供相應的接口方便驗證

二、測試用例

@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/hasRoles/hasAllRoles用於判斷用戶是否擁有某個角色/某些權限;可是沒有提供如hashAnyRole用於判斷是否有某些權限中的某一個。

Shiro提供的checkRole/checkRoles和hasRole/hasAllRoles不一樣的地方是它在判斷爲假的狀況下會拋出UnauthorizedException異常。

到此基於角色的訪問控制(即隱式角色)就完成了,這種方式的缺點就是若是不少地方進行了角色判斷,可是有一天不須要了那麼就須要修改相應代碼把全部相關的地方進行刪除,即耦合性太大;這就是粗粒度形成的問題。

基於資源的訪問控制(顯示角色)
一、在ini配置文件配置用戶擁有的角色及角色-權限關係(shiro-permission.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返回相應的權限信息。只須要維護「用戶——角色」之間的關係便可。

二、測試用例

@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用於判斷擁有某一個權限的接口。

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

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

受權流程:

流程以下:
一、首先調用Subject.isPermitted/hasRole接口,其會委託給SecurityManager,而SecurityManager接着會委託給Authorizer;
二、Authorizer是真正的受權者,若是咱們調用如isPermitted(「user:view」),其首先會經過PermissionResolver把字符串轉換成相應的Permission實例;
三、在進行受權以前,其會調用相應的Realm獲取Subject相應的角色/權限用於匹配傳入的角色/權限;
四、Authorizer會判斷Realm的角色/權限是否和傳入的匹配,若是有多個Realm,會委託給ModularRealmAuthorizer進行循環判斷,若是匹配如isPermitted/hasRole會返回true,不然返回false表示受權失敗。

ModularRealmAuthorizer進行多Realm匹配流程:
一、首先檢查相應的Realm是否實現了實現了Authorizer;
二、若是實現了Authorizer,那麼接着調用其相應的isPermitted/hasRole接口進行匹配;
三、若是有一個Realm匹配那麼將返回true,不然返回false。

若是Realm進行受權的話,應該繼承AuthorizingRealm,其流程是:
1.一、若是調用hasRole*,則直接獲取AuthorizationInfo.getRoles()與傳入的角色比較便可;
1.二、首先若是調用如isPermitted(「user:view」),首先經過PermissionResolver將權限字符串轉換成相應的Permission實例,默認使用WildcardPermissionResolver,即轉換爲通配符的WildcardPermission;
二、經過AuthorizationInfo.getObjectPermissions()獲得Permission實例集合;經過AuthorizationInfo. getStringPermissions()獲得字符串集合並經過PermissionResolver解析爲Permission實例;而後獲取用戶的角色,並經過RolePermissionResolver解析角色對應的權限集合(默認沒有實現,能夠本身提供);
三、接着調用Permission. implies(Permission p)逐個與傳入的權限比較,若是有匹配的則返回true,不然false。

Authorizer、PermissionResolver及RolePermissionResolver
Authorizer的職責是進行受權(訪問控制),是Shiro API中受權核心的入口點,其提供了相應的角色/權限判斷接口。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=org.apache.shiro.authz.permission.WildcardPermissionResolver
authorizer.permissionResolver=$permissionResolver

設置ModularRealmAuthorizer的rolePermissionResolver,其會自動設置到相應的Realm上(其實現了RolePermissionResolverAware接口),如:
rolePermissionResolver=com.github.zhangkaitao.shiro.chapter3.permission.MyRolePermissionResolver
authorizer.rolePermissionResolver=$rolePermissionResolver

示例:

一、ini配置(shiro-authorizer.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

//自定義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形成影響。

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

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。

三、定義MyRolePermissionResolver
RolePermissionResolver用於根據角色字符串來解析獲得權限集合。

public class MyRolePermissionResolver implements RolePermissionResolver {
@Override
public Collection resolvePermissionsInRole(String roleString) {
if("role1".equals(roleString)) {
return Arrays.asList((Permission)new WildcardPermission("menu:*"));
}
return null;
}
}

此處的實現很簡單,若是用戶擁有role1,那麼就返回一個「menu:*」的權限。

四、自定義Realm

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):表示根據用戶身份獲取受權信息。
這種方式的好處是當只須要身份驗證時只須要獲取身份驗證信息而不須要獲取受權信息。

另外咱們可使用JdbcRealm,須要作的操做以下:
一、執行sql/ shiro-init-data.sql 插入相關的權限數據;
二、使用shiro-jdbc-authorizer.ini配置文件,須要設置jdbcRealm.permissionsLookupEnabled
爲true來開啓權限查詢。

這次還要注意就是不能把咱們自定義的如「+user1+10」配置到INI配置文件,即便有IniRealm完成,由於IniRealm在new完成後就會解析這些權限字符串,默認使用了WildcardPermissionResolver完成,即此處是一個設計權限,若是採用生命週期(如使用初始化方法)的方式進行加載就能夠解決咱們自定義permissionResolver的問題。

五、測試用例:

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裏的驗證邏輯實現的

相關文章
相關標籤/搜索