shiro安全框架的核心就是認證和授權,前面已談到關于restful的改造,本文主要談一下認證和授權過程,以及粗粒度和細粒度的授權等。
參考:https://blog.csdn.net/johnstrive/article/details/74741783
權限管理
基本上涉及到用戶參與的系統(tǒng)都要權限管理,權限管理屬于系統(tǒng)安全的范疇,權限管理實現對用戶訪問系統(tǒng)的控制,按照安全規(guī)則或者安全策略控制用戶可以訪問而且只能訪問自己被授權的資源。
權限管理包括身份認證和授權兩部分,簡稱認證授權。對于需要訪問控制的資源用戶首先經過身份認證,認證通過后用戶具有該資源的訪問權限方可訪問。
身份認證
判斷一個用戶是否為合法用戶的處理過程。最常用的簡單身份認證方式是系統(tǒng)通過核對用戶輸入的用戶名和口令,看其是否與系統(tǒng)中存儲的該用戶的用戶名和口令一致,來判斷用戶身份是否正確。對于采用指紋等系統(tǒng),則出示指紋;對于硬件Key等刷卡系統(tǒng),則需要刷卡。
認證關鍵對象
- Subject:主體
訪問系統(tǒng)的用戶,主體可以是用戶、程序等,進行認證的都稱為主體; - Principal:身份信息
是主體(subject)進行身份認證的標識,標識必須具有唯一性,如用戶名、手機號、郵箱地址等,一個主體可以有多個身份,但是必須有一個主身份(Primary Principal)。 - credential:憑證信息
是只有主體自己知道的安全信息,如密碼、證書等。
授權
授權,即訪問控制,控制誰能訪問哪些資源。主體進行身份認證后需要分配權限方可訪問系統(tǒng)的資源,對于某些資源沒有權限是無法訪問的。
授權關鍵對象
授權可簡單理解為 who 對 what(which) 進行 How 操作:
- Who,即主體(Subject),主體需要訪問系統(tǒng)中的資源。
- What,即資源(Resource),如系統(tǒng)菜單、頁面、按鈕、類方法、系統(tǒng)商品信息等。資源包括資源類型和資源實例,比如商品信息為資源類型,類型為t01的商品為資源實例,編號為001的商品信息也屬于資源實例。
- How,權限/許可(Permission),規(guī)定了主體對資源的操作許可,權限離開資源沒有意義,如用戶查詢權限、用戶添加權限、某個類方法的調用權限、編號為001用戶的修改權限等,通過權限可知主體對哪些資源都有哪些操作許可。
權限分為粗顆粒和細顆粒,粗顆粒權限是指對資源類型的權限,細顆粒權限是對資源實例的權限。
權限模型
對上節(jié)中的主體、資源、權限通過數據模型表示。
- 主體(賬號、密碼)
- 角色(角色名稱)
- 主體和角色關系(主體id、角色id)
- 權限(權限名稱、資源id)
- 角色和權限關系(角色id、權限id)
- 資源(資源id、訪問地址)
如下圖:
權限控制
基于角色的訪問控制
RBAC基于角色的訪問控制(Role-Based Access Control)是以角色為中心進行訪問控制,比如:主體的角色為總經理可以查詢企業(yè)運營報表,查詢員工工資信息等,訪問控制流程如下:
圖中的判斷邏輯代碼可以理解為:
if(主體.hasRole("總經理角色id")){
查詢工資
}
缺點:以角色進行訪問控制粒度較粗,如果上圖中查詢工資所需要的角色變化為總經理和部門經理,此時就需要修改判斷邏輯為“判斷主體的角色是否是總經理或部門經理”,系統(tǒng)可擴展性差。
修改代碼如下:
if(主體.hasRole("總經理角色id") || 主體.hasRole("部門經理角色id")){
查詢工資
}
基于資源的訪問控制
RBAC基于資源的訪問控制(Resource-Based Access Control)是以資源為中心進行訪問控制,比如:主體必須具有查詢工資權限才可以查詢員工工資信息等,訪問控制流程如下:
上圖中的判斷邏輯代碼可以理解為:
if(主體.hasPermission("wage:query")){
查詢工資
}
優(yōu)點:系統(tǒng)設計時定義好查詢工資的權限標識,即使查詢工資所需要的角色變化為總經理和部門經理也只需要將“查詢工資信息權限”添加到“部門經理角色”的權限列表中,判斷邏輯不用修改,系統(tǒng)可擴展性強。
粗顆粒度和細顆粒度
什么是粗顆粒度和細顆粒度
對資源類型的管理稱為粗顆粒度權限管理,即只控制到菜單、按鈕、方法,粗粒度的例子比如:用戶具有用戶管理的權限,具有導出訂單明細的權限。對資源實例的控制稱為細顆粒度權限管理,即控制到數據級別的權限,比如:用戶只允許修改本部門的員工信息,用戶只允許導出自己創(chuàng)建的訂單明細。
如何實現粗顆粒度和細顆粒度
對于粗顆粒度的權限管理可以很容易做系統(tǒng)架構級別的功能,即系統(tǒng)功能操作使用統(tǒng)一的粗顆粒度的權限管理。
對于細顆粒度的權限管理不建議做成系統(tǒng)架構級別的功能,因為對數據級別的控制是系統(tǒng)的業(yè)務需求,隨著業(yè)務需求的變更業(yè)務功能變化的可能性很大,建議對數據級別的權限控制在業(yè)務層個性化開發(fā),比如:用戶只允許修改自己創(chuàng)建的商品信息可以在service接口添加校驗實現,service接口需要傳入當前操作人的標識,與商品信息創(chuàng)建人標識對比,不一致則不允許修改商品信息。
基于url攔截
基于url攔截是企業(yè)中常用的權限管理方法,實現思路是:將系統(tǒng)操作的每個url配置在權限表中,將權限對應到角色,將角色分配給用戶,用戶訪問系統(tǒng)功能通過Filter進行過慮,過慮器獲取到用戶訪問的url,只要訪問的url是用戶分配角色中的url則放行繼續(xù)訪問。
如下圖:
Shiro中關鍵對象
-
Subject
即主體,外部應用與subject進行交互,subject記錄了當前操作用戶,將用戶的概念理解為當前操作的主體,可能是一個通過瀏覽器請求的用戶,也可能是一個運行的程序。 Subject在shiro中是一個接口,接口中定義了很多認證授權相關的方法,外部程序通過subject進行認證授,而subject是通過SecurityManager安全管理器進行認證授權 -
SecurityManager
即安全管理器,對全部的subject進行安全管理,它是shiro的核心,負責對所有的subject進行安全管理。通過SecurityManager可以完成subject的認證、授權等,實質上SecurityManager是通過Authenticator進行認證,通過Authorizer進行授權,通過SessionManager進行會話管理等。
SecurityManager是一個接口,繼承了Authenticator, Authorizer, SessionManager這三個接口。 -
Authenticator
即認證器,對用戶身份進行認證,Authenticator是一個接口,shiro提供ModularRealmAuthenticator實現類,通過ModularRealmAuthenticator基本上可以滿足大多數需求,也可以自定義認證器。 -
Authorizer
即授權器,用戶通過認證器認證通過,在訪問功能時需要通過授權器判斷用戶是否有此功能的操作權限。 -
realm
即領域,相當于datasource數據源,securityManager進行安全認證需要通過Realm獲取用戶權限數據,比如:如果用戶身份數據在數據庫那么realm就需要從數據庫獲取用戶身份信息。
注意:不要把realm理解成只是從數據源取數據,在realm中還有認證授權校驗的相關的代碼。 -
SessionManager
即會話管理,shiro框架定義了一套會話管理,它不依賴web容器的session,所以shiro可以使用在非web應用上,也可以將分布式應用的會話集中在一點管理,此特性可使它實現單點登錄。 -
SessionDAO
即會話dao,是對session會話操作的一套接口,比如要將session存儲到數據庫,可以通過jdbc將會話存儲到數據庫。 -
CacheManager
CacheManager即緩存管理,將用戶權限數據存儲在緩存,這樣可以提高性能。 -
Cryptography
即密碼管理,shiro提供了一套加密/解密的組件,方便開發(fā)。比如提供常用的散列、加/解密等功能。
Shiro認證流程
自定義Realm
認證時需要自己實現realm去獲取特定的數據,如驗證賬號密碼等。
public class CustomRealm1 extends AuthorizingRealm {
@Override
public String getName() {
return "customRealm1";
}
//支持UsernamePasswordToken
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
//認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
//從token中 獲取用戶身份信息
String username = (String) token.getPrincipal();
//拿username從數據庫中查詢
//....
//如果查詢不到則返回null
if(!username.equals("zhang")){//這里模擬查詢不到
return null;
}
//獲取從數據庫查詢出來的用戶密碼
String password = "123";//這里使用靜態(tài)數據模擬。。
//返回認證信息由父類AuthenticatingRealm進行認證
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
username, password, getName());
return simpleAuthenticationInfo;
}
//授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
// TODO Auto-generated method stub
return null;
}
}
注意,在SimpleAuthenticationInfo中第一個參數是Object類型,即你可以把User對象存入,在后面可以通過SecurityUtils.getSubject().getPrincipal()獲取用戶信息。
認證密碼加密
散列算法
一般用于生成一段文本的摘要信息,散列算法不可逆,將內容可以生成摘要,無法將摘要轉成原始內容。散列算法常用于對密碼進行散列,常用的散列算法有MD5、SHA。
一般散列算法需要提供一個salt(鹽)與原始內容生成摘要信息,這樣做的目的是為了安全性,比如:111111的md5值是:96e79218965eb72c92a549dd5a330112,拿著“96e79218965eb72c92a549dd5a330112”去md5破解網站很容易進行破解,如果要是對111111和salt(鹽,一個隨機數)進行散列,這樣雖然密碼都是111111加不同的鹽會生成不同的散列值。
代碼如下:
//md5加密,不加鹽
String password_md5 = new Md5Hash("111111").toString();
System.out.println("md5加密,不加鹽="+password_md5);
//md5加密,加鹽,一次散列
String password_md5_sale_1 = new Md5Hash("111111", "eteokues", 1).toString();
System.out.println("password_md5_sale_1="+password_md5_sale_1);
String password_md5_sale_2 = new Md5Hash("111111", "uiwueylm", 1).toString();
System.out.println("password_md5_sale_2="+password_md5_sale_2);
//兩次散列相當于md5(md5())
//使用SimpleHash
String simpleHash = new SimpleHash("MD5", "111111", "eteokues",1).toString();
System.out.println(simpleHash);
在realm中使用
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
//用戶賬號
String username = (String) token.getPrincipal();
//根據用戶賬號從數據庫取出鹽和加密后的值
//..這里使用靜態(tài)數據
//如果根據賬號沒有找到用戶信息則返回null,shiro拋出異常“賬號不存在”
//按照固定規(guī)則加密碼結果 ,此密碼 要在數據庫存儲,原始密碼 是111111,鹽是eteokues
String password = "cb571f7bd7a6f73ab004a70322b963d5";
//鹽,隨機數,此隨機數也在數據庫存儲
String salt = "eteokues";
//返回認證信息
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
username, password, ByteSource.Util.bytes(salt),getName());
return simpleAuthenticationInfo;
}
Shiro授權
授權流程
授權方式
Shiro 支持三種方式的授權:
- 編程式:通過寫if/else 授權代碼塊完成:
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
//有權限
} else {
//無權限
}
- 注解式:通過在執(zhí)行的Java方法上放置相應的注解完成:
@RequiresRoles("admin")
public void hello() {
//有權限
}
- JSP/GSP 標簽:在JSP/GSP 頁面通過相應的標簽完成:
<shiro:hasRole name="admin">
<!— 有權限—>
</shiro:hasRole>
Permission 鑒權方式
我們了解到了 Shiro 的Authorization有三種方式,作為細粒度化的 Authorization,Permission 同樣也支持粗粒度的 Authorization 的三種方式即代碼判斷,注解,JSP頁面校驗。
- 代碼判斷/注解
@RequiresRoles("admin")
@RequiresPermissions("admin:view:*")
@RequestMapping(value="/admin")
public String AuthorizationOne () {
Subject admin =SecurityUtils.getSubject();
System.out.println("角色 " + admin.getPrincipal());
admin.isPermitted("admin:view:*");
System.out.println("角色 " + admin.getPrincipal()+" 是否擁有 admin:view:* 權限:"+admin.isPermitted("admin:view:*"));
return "admin";
}
- JSP 頁面校驗
<!-- 只有 user:view:* 權限才能顯示一下內容 -->
<shiro:hasPermission name="user:view:*">
Only User has 'user:view:*' can access to those words
</shiro:hasPermission>
<br>
<!-- 只有 admin:view:* 權限才能顯示一下內容 -->
<shiro:hasPermission name="admin:view:*">
Only Admin has 'admin:view:*' can access to those words
</shiro:hasPermission>
Shiro Authorization 大致可以被概述為實現了角色授權和權限授權
- 角色授權:粗粒度授權,為當前Subject 做角色判定或賦予
- 權限授權:細粒度授權,為當前Role 做權限判定或賦予。
- 在細粒度授權時要重分理解 資源標識符:操作:對象實例ID 規(guī)則的定義的應用的實際場景。
權限字符串規(guī)則
權限一般是以字符串的形式表示的,權限字符串的規(guī)則是:“資源標識符:操作:資源實例標識符”,意思是對哪個資源的哪個實例具有什么操作,“:”是資源/操作/實例的分割符,, 表示操作的分割,* 表示任意資源/操作/實例。
如下:
用戶創(chuàng)建權限:user:create,或user:create:*
用戶修改實例001的權限:user:update:001
用戶實例001的所有權限:user:*:001
資源-操作-實例
資源-操作-實例 是 Shiro 做細粒度鑒權 persmission時的一種規(guī)則。
- 擴展
默認支持通配符權限字符串,: 表示資源/操作/實例的分割;, 表示操作的分割,* 表示任意資源/操作/實例。 - 單個權限
- user:query、user:edit。
- 冒號是一個特殊字符,它用來分隔權限字符串的下一部件:第一部分是權限被操作的領域,第二部分是被執(zhí)行的操作。
- 多個值:每個部件能夠保護多個值。因此,除了授予用戶 user:query和 user:edit 權限外,也可以簡單地授予他們一個:user:query, edit。
- 還可以用 * 號代替所有的值,如:user:* , 也可以寫:*:query,表示某個用戶在所有的領域都有 query 的權限。
- 例子
- 單個資源多個權限 user:query user:add 多值 user:query,add
- 單個資源所有權限 user:query,add,update,delete user:*
- 所有資源某個權限 *:view
- 實例級訪問控制
- 規(guī)則: 資源標識符:操作:對象實例 ID
- 這種情況通常會使用三個部件:域、操作、被付諸實施的實例。如:user:edit:manager
- 也可以使用通配符來定義,如:user:edit:、user::、user::manager
- 部分省略通配符:缺少的部件意味著用戶可以訪問所有與之匹配的值,比如:user:edit 等價于 user:edit :、
user 等價于 user: :* - 通配符只能從字符串的結尾處省略部件,也就是說 user:edit 并不等價于 user:*:edit
- 例子
- 單個實例的單個權限 printer:query:lp7200 printer:print:epsoncolor
- 對資源printer的lp7200實例擁有query權限。
- 對資源printer的epsoncolor實例擁有query權限。
- 所有實例的單個權限 printer:print:*
- 對資源printer的所有r實例擁有query權限。
- 所有實例的所有權限 printer::
- 對資源printer的1實例擁有所有權限。然后通過如下代碼判斷
subject().checkPermissions(“printer:setting:1”, “printer:printe:2”);
- 對資源printer的1實例擁有所有權限。然后通過如下代碼判斷
- 單個實例的所有權限 printer:*:lp7200
- 對資源printer的lp7200實例擁有所有權限
- 單個實例的多個權限 printer:query,print:lp7200
- 對資源printer的lp7200實例擁有query,print權限
- 單個實例的單個權限 printer:query:lp7200 printer:print:epsoncolor
基于角色的授權
// 用戶授權檢測 基于角色授權
// 是否有某一個角色
System.out.println("用戶是否擁有一個角色:" + subject.hasRole("role1"));
// 是否有多個角色
System.out.println("用戶是否擁有多個角色:" + subject.hasAllRoles(Arrays.asList("role1", "role2")));
對應的check方法:
subject.checkRole("role1");
subject.checkRoles(Arrays.asList("role1", "role2"));
基于資源授權
// 基于資源授權
System.out.println("是否擁有某一個權限:" + subject.isPermitted("user:delete"));
System.out.println("是否擁有多個權限:" + subject.isPermittedAll("user:create:1", "user:delete"));
對應的check方法:
subject.checkPermission("sys:user:delete");
subject.checkPermissions("user:create:1","user:delete");
自定義realm
與上邊認證自定義realm一樣,大部分情況是要從數據庫獲取權限數據,這里直接實現基于資源的授權。
在認證章節(jié)寫的自定義realm類中完善doGetAuthorizationInfo方法,此方法需要完成:根據用戶身份信息從數據庫查詢權限字符串,由shiro進行授權。
// 授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
// 獲取身份信息
String username = (String) principals.getPrimaryPrincipal();
// 根據身份信息從數據庫中查詢權限數據
//....這里使用靜態(tài)數據模擬
List<String> permissions = new ArrayList<String>();
permissions.add("user:create");
permissions.add("user.delete");
//將權限信息封閉為AuthorizationInfo
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for(String permission:permissions){
simpleAuthorizationInfo.addStringPermission(permission);
}
return simpleAuthorizationInfo;
}
自定義permission和RolePerminssion
通過addStringPermission 默認是用Permission的實現類封裝的 當然也可以實現自定義的Permission。
public class MyPermission implements Permission {
String permissionCode;
public MyPermission(String name) {
permissionCode=name;
}
@Override
public boolean implies(Permission permission) {
//自定義比較
// TODO Auto-generated method stub
return false;
}
}
當我們調用subject.isPermitted("user:update")會調用將指令傳達給SecurityManager,SecurityManager 再將指令傳達給授權管理類Authorizer,Authorizer會通過reaml獲得授權信息SimpleAuthorizationInfo如果我們返回的授權信息擁有角色 會調用RolePermissionResolver實現類的方法 將角色的權限追加到SimpleAuthorizationInfo(默認是沒有實現的)。
public class MyRolePermissionResolver implements RolePermissionResolver{
@Override
public Collection<Permission> resolvePermissionsInRole(String roleString) {
// TODO Auto-generated method stub
return Arrays.asList((Permission)new MyPermission("menu:*"));
}
這里面是根據角色查詢權限
最終 遍歷SimpleAuthorizationInfo的權限信息 (我們的權限信息都封裝Permission接口實現類 調用implies方法進行比較 如果比較成功返回true 表示授權通過)自定義Permission的好處就是我們可以自定義匹配規(guī)則。
@RequiresPermissions 注解說明
@RequiresAuthentication
驗證用戶是否登錄,等同于方法subject.isAuthenticated() 結果為true時。
@RequiresUser
驗證用戶是否被記憶,user有兩種含義:
一種是成功登錄的(subject.isAuthenticated() 結果為true);
另外一種是被記憶的(subject.isRemembered()結果為true)。
@RequiresGuest
驗證是否是一個guest的請求,與@RequiresUser完全相反。
換言之,RequiresUser == !RequiresGuest。
此時subject.getPrincipal() 結果為null.
@RequiresRoles
例如:@RequiresRoles("aRoleName");
void someMethod();
如果subject中有aRoleName角色才可以訪問方法someMethod。如果沒有這個權限則會拋出異常AuthorizationException。
@RequiresPermissions
例如: @RequiresPermissions({"file:read", "write:aFile.txt"} )
void someMethod();
要求subject中必須同時含有file:read和write:aFile.txt的權限才能執(zhí)行方法someMethod()。否則拋出異常AuthorizationException。