授權(quán),也叫訪問控制,即在應(yīng)用中控制誰能訪問哪些資源(如訪問頁面/編輯數(shù)據(jù)/頁面操作等)。在授權(quán)中需了解的幾個(gè)關(guān)鍵對(duì)象:主體(Subject)、資源(Resource)、權(quán)限(Permission)、角色(Role)。
主體 主體,即訪問應(yīng)用的用戶,在 Shiro 中使用 Subject 代表該用戶。用戶只有授權(quán)后才允許訪問相應(yīng)的資源。
資源 在應(yīng)用中用戶可以訪問的URL,比如訪問 JSP 頁面、查看/編輯某些數(shù)據(jù)、訪問某個(gè)業(yè)務(wù)方法、打印文本等等都是資源。用戶只要授權(quán)后才能訪問。
權(quán)限 安全策略中的原子授權(quán)單位,通過權(quán)限我們可以表示在應(yīng)用中用戶有沒有操作某個(gè)資源的權(quán)力。即權(quán)限表示在應(yīng)用中用戶能不能訪問某個(gè)資源,如: 訪問用戶列表頁面 查看/新增/修改/刪除用戶數(shù)據(jù)(即很多時(shí)候都是 CRUD(增查改刪)式權(quán)限控制)打印文檔等。如上可以看出,權(quán)限代表了用戶有沒有操作某個(gè)資源的權(quán)利,即反映在某個(gè)資源上的操作允不允許,不反映誰去執(zhí)行這個(gè)操作。所以后續(xù)還需要把權(quán)限賦予給用戶,即定義哪個(gè)用戶允許在某個(gè)資源上做什么操作(權(quán)限),Shiro 不會(huì)去做這件事情,而是由實(shí)現(xiàn)人員提供。
Shiro 支持粗粒度權(quán)限(如用戶模塊的所有權(quán)限)和細(xì)粒度權(quán)限(操作某個(gè)用戶的權(quán)限,即實(shí)例級(jí)別的),后續(xù)部分介紹。
角色 角色代表了操作集合,可以理解為權(quán)限的集合,一般情況下我們會(huì)賦予用戶角色而不是權(quán)限,即這樣用戶可以擁有一組權(quán)限,賦予權(quán)限時(shí)比較方便。典型的如:項(xiàng)目經(jīng)理、技術(shù)總監(jiān)、CTO、開發(fā)工程師等都是角色,不同的角色擁有一組不同的權(quán)限。
隱式角色: 即直接通過角色來驗(yàn)證用戶有沒有操作權(quán)限,如在應(yīng)用中 CTO、技術(shù)總監(jiān)、開發(fā)工程師可以使用打印機(jī),假設(shè)某天不允許開發(fā)工程師使用打印機(jī),此時(shí)需要從應(yīng)用中刪除相應(yīng)代碼;再如在應(yīng)用中 CTO、技術(shù)總監(jiān)可以查看用戶、查看權(quán)限;突然有一天不允許技術(shù)總監(jiān)查看用戶、查看權(quán)限了,需要在相關(guān)代碼中把技術(shù)總監(jiān)角色從判斷邏輯中刪除掉;即粒度是以角色為單位進(jìn)行訪問控制的,粒度較粗;如果進(jìn)行修改可能造成多處代碼修改。
顯示角色: 在程序中通過權(quán)限控制誰能訪問某個(gè)資源,角色聚合一組權(quán)限集合;這樣假設(shè)哪個(gè)角色不能訪問某個(gè)資源,只需要從角色代表的權(quán)限集合中移除即可;無須修改多處代碼;即粒度是以資源/實(shí)例為單位的;粒度較細(xì)。
請 google 搜索“RBAC”和“RBAC新解”分別了解“基于角色的訪問控制”“基于資源的訪問控制(Resource-Based Access Control)”。
Shiro 支持三種方式的授權(quán):
編程式:通過寫 if/else 授權(quán)代碼塊完成:
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
//有權(quán)限
} else {
//無權(quán)限
}
注解式:通過在執(zhí)行的 Java 方法上放置相應(yīng)的注解完成:
@RequiresRoles("admin")
public void hello() {
//有權(quán)限
}
沒有權(quán)限將拋出相應(yīng)的異常;
JSP/GSP 標(biāo)簽:在 JSP/GSP 頁面通過相應(yīng)的標(biāo)簽完成:
<shiro:hasRole name="admin">
<!— 有權(quán)限 —>
</shiro:hasRole>
后續(xù)部分將詳細(xì)介紹如何使用。
基于角色的訪問控制(隱式角色)
1、在 ini 配置文件配置用戶擁有的角色(shiro-role.ini)
[users]
zhang=123,role1,role2
wang=123,role1
規(guī)則即:“用戶名=密碼,角色1,角色2”,如果需要在應(yīng)用中判斷用戶是否有相應(yīng)角色,就需要在相應(yīng)的 Realm 中返回角色信息,也就是說 Shiro 不負(fù)責(zé)維護(hù)用戶-角色信息,需要應(yīng)用提供,Shiro 只是提供相應(yīng)的接口方便驗(yàn)證,后續(xù)會(huì)介紹如何動(dòng)態(tài)的獲取用戶角色。
2、測試用例(com.github.zhangkaitao.shiro.chapter3.RoleTest)
@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 用于判斷用戶是否擁有某個(gè)角色/某些權(quán)限;但是沒有提供如 hashAnyRole 用于判斷是否有某些權(quán)限中的某一個(gè)。
@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 不同的地方是 checkRole/checkRoles 在判斷為假的情況下會(huì)拋出 UnauthorizedException 異常。
到此基于角色的訪問控制(即隱式角色)就完成了,這種方式的缺點(diǎn)就是如果很多地方進(jìn)行了角色判斷,但是有一天不需要了那么就需要修改相應(yīng)代碼把所有相關(guān)的地方進(jìn)行刪除;這就是粗粒度造成的問題。
基于資源的訪問控制(顯示角色)
1、在 ini 配置文件配置用戶擁有的角色及角色-權(quán)限關(guān)系(shiro-permission.ini)
[users]
zhang=123,role1,role2
wang=123,role1
[roles]
role1=user:create,user:update
role2=user:create,user:delete
規(guī)則:“用戶名=密碼,角色 1,角色 2”“角色=權(quán)限 1,權(quán)限 2”,即首先根據(jù)用戶名找到角色,然后根據(jù)角色再找到權(quán)限;即角色是權(quán)限集合;Shiro 同樣不進(jìn)行權(quán)限的維護(hù),需要我們通過 Realm 返回相應(yīng)的權(quán)限信息。只需要維護(hù)“用戶——角色”之間的關(guān)系即可。
2、測試用例(com.github.zhangkaitao.shiro.chapter3.PermissionTest)
@Test
public void testIsPermitted() {
login("classpath:shiro-permission.ini", "zhang", "123");
//判斷擁有權(quán)限:user:create
Assert.assertTrue(subject().isPermitted("user:create"));
//判斷擁有權(quán)限:user:update and user:delete
Assert.assertTrue(subject().isPermittedAll("user:update", "user:delete"));
//判斷沒有權(quán)限:user:view
Assert.assertFalse(subject().isPermitted("user:view"));
}
Shiro 提供了 isPermitted 和 isPermittedAll 用于判斷用戶是否擁有某個(gè)權(quán)限或所有權(quán)限,也沒有提供如 isPermittedAny 用于判斷擁有某一個(gè)權(quán)限的接口。
@Test(expected = UnauthorizedException.class) public void testCheckPermission () { login("classpath:shiro-permission.ini", "zhang", "123"); //斷言擁有權(quán)限:user:create subject().checkPermission("user:create"); //斷言擁有權(quán)限:user:delete and user:update subject().checkPermissions("user:delete", "user:update"); //斷言擁有權(quán)限:user:view 失敗拋出異常 subject().checkPermissions("user:view"); }
但是失敗的情況下會(huì)拋出 UnauthorizedException 異常。
到此基于資源的訪問控制(顯示角色)就完成了,也可以叫基于權(quán)限的訪問控制,這種方式的一般規(guī)則是“資源標(biāo)識(shí)符:操作”,即是資源級(jí)別的粒度;這種方式的好處就是如果要修改基本都是一個(gè)資源級(jí)別的修改,不會(huì)對(duì)其他模塊代碼產(chǎn)生影響,粒度小。但是實(shí)現(xiàn)起來可能稍微復(fù)雜點(diǎn),需要維護(hù)“用戶——角色,角色——權(quán)限(資源:操作)”之間的關(guān)系。
規(guī)則:“資源標(biāo)識(shí)符:操作:對(duì)象實(shí)例 ID” 即對(duì)哪個(gè)資源的哪個(gè)實(shí)例可以進(jìn)行什么操作。其默認(rèn)支持通配符權(quán)限字符串,“:”表示資源/操作/實(shí)例的分割;“,”表示操作的分割;“*”表示任意資源/操作/實(shí)例。
1、單個(gè)資源單個(gè)權(quán)限
subject().checkPermissions("system:user:update");
用戶擁有資源“system:user”的“update”權(quán)限。
2、單個(gè)資源多個(gè)權(quán)限
role41=system:user:update,system:user:delete
然后通過如下代碼判斷
subject().checkPermissions("system:user:update", "system:user:delete");
用戶擁有資源“system:user”的“update”和“delete”權(quán)限。如上可以簡寫成:
ini 配置(表示角色4擁有 system:user 資源的 update 和 delete 權(quán)限)
role42="system:user:update,delete"
接著可以通過如下代碼判斷
subject().checkPermissions("system:user:update,delete");
通過“system:user:update,delete”驗(yàn)證“system:user:update, system:user:delete”是沒問題的,但是反過來是規(guī)則不成立。
3、單個(gè)資源全部權(quán)限
ini 配置
role51="system:user:create,update,delete,view"
然后通過如下代碼判斷
subject().checkPermissions("system:user:create,update,delete,view");
用戶擁有資源“system:user”的“create”、“update”、“delete”和“view”所有權(quán)限。如上可以簡寫成:
ini 配置文件(表示角色 5 擁有 system:user 的所有權(quán)限)
role52=system:user:*
也可以簡寫為(推薦上邊的寫法):
role53=system:user
然后通過如下代碼判斷
subject().checkPermissions("system:user:*"); subject().checkPermissions("system:user");
通過“system:user:*”驗(yàn)證“system:user:create,delete,update:view”可以,但是反過來是不成立的。
4、所有資源單個(gè)權(quán)限
ini 配置
role61=*:view
然后通過如下代碼判斷
subject().checkPermissions("user:view");
用戶擁有所有資源的“view”所有權(quán)限。假設(shè)判斷的權(quán)限是“"system:user:view”,那么需要“role5=::view”這樣寫才行
5、實(shí)例級(jí)別的權(quán)限
ini 配置
role71=user:view:1
對(duì)資源 user 的 1 實(shí)例擁有 view 權(quán)限。
然后通過如下代碼判斷
subject().checkPermissions("user:view:1");
ini 配置
role72="user:update,delete:1"
對(duì)資源 user 的 1 實(shí)例擁有 update、delete 權(quán)限。
然后通過如下代碼判斷
subject().checkPermissions("user:delete,update:1"); subject().checkPermissions("user:update:1", "user:delete:1");
ini 配置
role73=user:*:1
對(duì)資源 user 的 1 實(shí)例擁有所有權(quán)限。
然后通過如下代碼判斷
subject().checkPermissions("user:update:1", "user:delete:1", "user:view:1");
ini 配置
role74=user:auth:*
對(duì)資源 user 的 1 實(shí)例擁有所有權(quán)限。
然后通過如下代碼判斷
subject().checkPermissions("user:auth:1", "user:auth:2");
ini 配置
role75=user:*:*
對(duì)資源 user 的 1 實(shí)例擁有所有權(quán)限。
然后通過如下代碼判斷
subject().checkPermissions("user:view:1", "user:auth:2");
6、Shiro 對(duì)權(quán)限字符串缺失部分的處理
如“user:view”等價(jià)于“user:view:*”;而“organization”等價(jià)于“organization:*”或者“organization:*:*”??梢赃@么理解,這種方式實(shí)現(xiàn)了前綴匹配。
另外如“user:*”可以匹配如“user:delete”、“user:delete”可以匹配如“user:delete:1”、“user:*:1”可以匹配如“user:view:1”、“user”可以匹配“user:view”或“user:view:1”等。即*可以匹配所有,不加*可以進(jìn)行前綴匹配;但是如“*:view”不能匹配“system:user:view”,需要使用“*:*:view”,即后綴匹配必須指定前綴(多個(gè)冒號(hào)就需要多個(gè)*來匹配)。
7、WildcardPermission
如下兩種方式是等價(jià)的:
subject().checkPermission("menu:view:1"); subject().checkPermission(new WildcardPermission("menu:view:1"));
因此沒什么必要的話使用字符串更方便。
8、性能問題
通配符匹配方式比字符串相等匹配來說是更復(fù)雜的,因此需要花費(fèi)更長時(shí)間,但是一般系統(tǒng)的權(quán)限不會(huì)太多,且可以配合緩存來提供其性能,如果這樣性能還達(dá)不到要求我們可以實(shí)現(xiàn)位操作算法實(shí)現(xiàn)性能更好的權(quán)限匹配。另外實(shí)例級(jí)別的權(quán)限驗(yàn)證如果數(shù)據(jù)量太大也不建議使用,可能造成查詢權(quán)限及匹配變慢??梢钥紤]比如在sql查詢時(shí)加上權(quán)限字符串之類的方式在查詢時(shí)就完成了權(quán)限匹配。
流程如下:
ModularRealmAuthorizer 進(jìn)行多 Realm 匹配流程:
如果 Realm 進(jìn)行授權(quán)的話,應(yīng)該繼承 AuthorizingRealm,其流程是:
Authorizer 的職責(zé)是進(jìn)行授權(quán)(訪問控制),是 Shiro API 中授權(quán)核心的入口點(diǎn),其提供了相應(yīng)的角色/權(quán)限判斷接口,具體請參考其 Javadoc。SecurityManager 繼承了 Authorizer 接口,且提供了 ModularRealmAuthorizer 用于多 Realm 時(shí)的授權(quán)匹配。PermissionResolver 用于解析權(quán)限字符串到 Permission 實(shí)例,而 RolePermissionResolver 用于根據(jù)角色解析相應(yīng)的權(quán)限集合。
我們可以通過如下 ini 配置更改 Authorizer 實(shí)現(xiàn):
authorizer=org.apache.shiro.authz.ModularRealmAuthorizer securityManager.authorizer=$authorizer
對(duì)于 ModularRealmAuthorizer,相應(yīng)的 AuthorizingSecurityManager 會(huì)在初始化完成后自動(dòng)將相應(yīng)的 realm 設(shè)置進(jìn)去,我們也可以通過調(diào)用其 setRealms() 方法進(jìn)行設(shè)置。對(duì)于實(shí)現(xiàn)自己的 authorizer 可以參考 ModularRealmAuthorizer 實(shí)現(xiàn)即可,在此就不提供示例了。
設(shè)置 ModularRealmAuthorizer 的 permissionResolver,其會(huì)自動(dòng)設(shè)置到相應(yīng)的 Realm 上(其實(shí)現(xiàn)了 PermissionResolverAware 接口),如:
permissionResolver=org.apache.shiro.authz.permission.WildcardPermissionResolver authorizer.permissionResolver=$permissionResolver
設(shè)置 ModularRealmAuthorizer 的 rolePermissionResolver,其會(huì)自動(dòng)設(shè)置到相應(yīng)的 Realm 上(其實(shí)現(xiàn)了 RolePermissionResolverAware 接口),如:
rolePermissionResolver=com.github.zhangkaitao.shiro.chapter3.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.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賦值之后(因?yàn)檎{(diào)用setRealms會(huì)將realms設(shè)置給authorizer,并給各個(gè)Realm設(shè)置permissionResolver和rolePermissionResolver) realm=com.github.zhangkaitao.shiro.chapter3.realm.MyRealm securityManager.realms=$realm
設(shè)置 securityManager 的 realms 一定要放到最后,因?yàn)樵谡{(diào)用 SecurityManager.setRealms 時(shí)會(huì)將 realms 設(shè)置給 authorizer,并為各個(gè) Realm 設(shè)置 permissionResolver 和 rolePermissionResolver。另外,不能使用 IniSecurityManagerFactory 創(chuàng)建的 IniRealm,因?yàn)槠涑跏蓟樞虻膯栴}可能造成后續(xù)的初始化 Permission 造成影響。
2、定義 BitAndWildPermissionResolver 及 BitPermission
BitPermission 用于實(shí)現(xiàn)位移方式的權(quán)限,如規(guī)則是:
權(quán)限字符串格式:+ 資源字符串 + 權(quán)限位 + 實(shí)例 ID;以 + 開頭中間通過 + 分割;權(quán)限:0 表示所有權(quán)限;1 新增(二進(jìn)制:0001)、2 修改(二進(jìn)制:0010)、4 刪除(二進(jìn)制:0100)、8 查看(二進(jìn)制:1000);如 +user+10 表示對(duì)資源 user 擁有修改 / 查看權(quán)限。
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) 方法用于判斷權(quán)限匹配的;
public class BitAndWildPermissionResolver implements PermissionResolver { @Override public Permission resolvePermission(String permissionString) { if(permissionString.startsWith("+")) { return new BitPermission(permissionString); } return new WildcardPermission(permissionString); } }
BitAndWildPermissionResolver 實(shí)現(xiàn)了 PermissionResolver 接口,并根據(jù)權(quán)限字符串是否以 “+” 開頭來解析權(quán)限字符串為 BitPermission 或 WildcardPermission。
3、定義 MyRolePermissionResolver
RolePermissionResolver 用于根據(jù)角色字符串來解析得到權(quán)限集合。
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; } }
此處的實(shí)現(xiàn)很簡單,如果用戶擁有 role1,那么就返回一個(gè) “menu:*” 的權(quán)限。
4、自定義 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代碼一樣,省略
}
}
此時(shí)我們繼承 AuthorizingRealm 而不是實(shí)現(xiàn) Realm 接口;推薦使用 AuthorizingRealm,因?yàn)椋?AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token):表示獲取身份驗(yàn)證信息;AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals):表示根據(jù)用戶身份獲取授權(quán)信息。這種方式的好處是當(dāng)只需要身份驗(yàn)證時(shí)只需要獲取身份驗(yàn)證信息而不需要獲取授權(quán)信息。對(duì)于 AuthenticationInfo 和 AuthorizationInfo 請參考其 Javadoc 獲取相關(guān)接口信息。
另外我們可以使用 JdbcRealm,需要做的操作如下:
此次還要注意就是不能把我們自定義的如 “+user1+10” 配置到 INI 配置文件,即使有 IniRealm 完成,因?yàn)?IniRealm 在 new 完成后就會(huì)解析這些權(quán)限字符串,默認(rèn)使用了WildcardPermissionResolver 完成,即此處是一個(gè)設(shè)計(jì)權(quán)限,如果采用生命周期(如使用初始化方法)的方式進(jìn)行加載就可以解決我們自定義 permissionResolver 的問題。
5、測試用例
public class AuthorizerTest extends BaseTest {
@Test
public void testIsPermitted() {
login("classpath:shiro-authorizer.ini", "zhang", "123");
//判斷擁有權(quán)限:user:create
Assert.assertTrue(subject().isPermitted("user1:update"));
Assert.assertTrue(subject().isPermitted("user2:update"));
//通過二進(jìn)制位的方式表示權(quán)限
Assert.assertTrue(subject().isPermitted("+user1+2"));//新增權(quán)限
Assert.assertTrue(subject().isPermitted("+user1+8"));//查看權(quán)限
Assert.assertTrue(subject().isPermitted("+user2+10"));//新增及查看
Assert.assertFalse(subject().isPermitted("+user1+4"));//沒有刪除權(quán)限
Assert.assertTrue(subject().isPermitted("menu:view"));//通過MyRolePermissionResolver解析得到的權(quán)限
}
}
通過如上步驟可以實(shí)現(xiàn)自定義權(quán)限驗(yàn)證了。另外因?yàn)椴恢С?hasAnyRole/isPermittedAny 這種方式的授權(quán),可以參考我的一篇《簡單 shiro 擴(kuò)展實(shí)現(xiàn) NOT、AND、OR 權(quán)限驗(yàn)證 》進(jìn)行簡單的擴(kuò)展完成這個(gè)需求,在這篇文章中通過重寫 AuthorizingRealm 里的驗(yàn)證邏輯實(shí)現(xiàn)的。
更多建議: