1、前言
Apache Shiro是一個強大且易用的Java安全架構,可以完成認證、授權、加密、會話管理、與Web 內建、緩存等。對比Spring Security,沒有Spring Security功能強大,但小而簡單的Shiro。
2、特性
可完成認證、授權、加密、會話管理、與Web內建、緩存等。
3、功能圖
1)4大主要功能[four primary concerns]
Authentication [ɔːˌθentɪˈkeɪʃn]:身份認證/登入,驗證使用者是不是擁有相應的身份;
Authorization [ˌɔːθəraɪˈzeɪʃn]:授權,即權限驗證,驗證某個已認證的使用者是否擁有某個權限;即判斷使用者是否能做事情,常見的如:驗證某個使用者是否擁有某個角色。或者細粒度的驗證某個使用者對某個資源是否具有某個權限;
Session Manager:會話管理,即使用者登入後就是一次會話,在沒有退出之前,它的所有資訊都在會話中;會話可以是普通JavaSE環境的,也可以是如Web環境的;
Cryptography [krɪpˈtɑːɡrəfi]:加密,保護資料的安全性,如密碼加密存儲到資料庫,而不是明文存儲;
2)以下為輔助功能[some auxiliary features]
Web Support:Web支援,可以非常容易的內建到Web環境;
Caching:緩存,比如使用者登入後,其使用者資訊、擁有的角色/權限不必每次去查,這樣可以提高效率;
Concurrency [kənˈkɚrənsi]:shiro支援多線程應用的并發驗證,即如在一個線程中開啟另一個線程,能把權限自動傳播過去;
Testing:提供測試支援;
Run As:允許一個使用者假裝為另一個使用者(如果他們允許)的身份進行通路;
Remember Me:記住我,這個是非常常見的功能,即一次登入後,下次再來的話不用登入了。
4、Shiro外部結構
可以看到:應用代碼直接互動的對象是Subject,也就是說Shiro的對外API核心就是Subject;其每個API的含義:
Subject:主體,代表了目前“使用者”,這個使用者不一定是一個具體的人,與目前應用互動的任何東西都是Subject,如網絡爬蟲,機器人等;即一個抽象概念;所有Subject都綁定到SecurityManager,與Subject的所有互動都會委托給SecurityManager;可以把Subject認為是一個門面;SecurityManager才是實際的執行者;
SecurityManager:安全管理器;即所有與安全有關的操作都會與SecurityManager互動;且它管理着所有Subject;可以看出它是Shiro的核心,它負責與後邊介紹的其他元件進行互動,如果學習過SpringMVC,你可以把它看成DispatcherServlet前端控制器;
Realm:域,Shiro從從Realm擷取安全資料(如使用者、角色、權限),就是說SecurityManager要驗證使用者身份,那麼它需要從Realm擷取相應的使用者進行比較以确定使用者身份是否合法;也需要從Realm得到使用者相應的角色/權限進行驗證使用者是否能進行操作;可以把Realm看成DataSource,即安全資料源。
也就是說對于我們而言,最簡單的一個Shiro應用:
1、應用代碼通過Subject來進行認證和授權,而Subject又委托給SecurityManager;
2、我們需要給Shiro的SecurityManager注入Realm,進而讓SecurityManager能得到合法的使用者及其權限進行判斷。
5、Shiro内部架構
Subject:主體,可以看到主體可以是任何可以與應用互動的“使用者”;
SecurityManager: Shiro的心髒;所有具體的互動都通過SecurityManager進行控制;它管理着所有Subject、且負責進行認證和授權、及會話、緩存的管理。
Authenticator:認證器,負責主體認證的。
Authrizer:授權器,或者通路控制器,用來決定主體是否有權限進行相應的操作;即控制着使用者能通路應用中的哪些功能;
Realm:可以有1個或多個Realm,可以認為是安全實體資料源,即用于擷取安全實體的;可以是JDBC實作,也可以是LDAP實作,或者記憶體實作等等;由使用者提供;注意:Shiro不知道你的使用者/權限存儲在哪及以何種格式存儲;是以我們一般在應用中都需要實作自己的Realm;
SessionManager:Shiro就抽象自己的Session來管理主體與應用之間互動的資料;這樣的話,比如我們在Web環境用,剛開始是一台Web伺服器;接着又上了台EJB伺服器;這時想把兩台伺服器的會話資料放到一個地方,這個時候就可以實作自己的分布式會話(如把資料放到Memcached伺服器);
SessionDAO:DAO大家都用過,資料通路對象,用于會話的CRUD,比如我們想把Session儲存到資料庫,那麼可以實作自己的SessionDAO,通過如JDBC寫到資料庫;比如想把Session放到Memcached中,可以實作自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache進行緩存,以提高性能;
CacheManager:緩存控制器,來管理如使用者、角色、權限等的緩存的;因為這些資料基本上很少去改變,放到緩存中後可以提高通路的性能
Cryptography:密碼子產品,Shiro提高了一些常見的加密元件用于如密碼加密/解密的。
6、身份驗證
身份驗證,即在應用中證明他就是他本人。一般提供如他們的身份ID 一些辨別資訊來表明他就是他本人,如提供身份證,使用者名/密碼來證明。在 shiro 中,使用者需要提供principals [ˈprɪnsəpəlz]-身份和credentials [krəˈdenʃlz]證明給shiro,進而應用能驗證使用者身份:
principals:身份,即主體的辨別屬性,可以是使用者名、郵箱等,唯一即可。一個主體可以有多個principals,但隻有一個Primary principals,一般是使用者名/密碼/手機号。
credentials:證明/憑證,即隻有主體知道的安全值,如密碼/數字證書等。最常見的principals和credentials組合就是使用者名/密碼。
DEMO:登入、退出 官網:http://shiro.apache.org/
1、首先準備一些使用者身份/憑據(shiro.ini),ini配置檔案通過[users]指定兩個主體
[users]
zhang=123
wang=123
2、測試用例org.apache.shiro.mgt.SecurityManager:SecurityManager
//1 init SecurityManager工廠
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//2 擷取SecurityManager工廠
SecurityManager securityManager = factory.getInstance();
//3 得到SecurityManager執行個體并綁定給SecurityUtils SecurityUtils.setSecurityManager(securityManager);
//4 得到Subject及建立使用者名/密碼身份驗證Token(即使用者身份/憑證)
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123");
try {subject.login(token); //5 登入,即身份驗證} catch (AuthenticationException e) {}
Assert.assertEquals(true, subject.isAuthenticated()); //斷言使用者已經登入
subject.logout();//6、退出
[點選并拖拽以移動]
6.1 身份認證流程
1、首先調用Subject.login(token)進行登入,其會自動委托給Security Manager,調用之前必
須通過SecurityUtils. setSecurityManager()設定;
2、SecurityManager負責真正的身份驗證邏輯;它會委托給Authenticator進行身份驗證;
3、Authenticator才是真正的身份驗證者,Shiro API中核心的身份認證入口點,此處可以自
定義插入自己的實作;
4、Authenticator可能會委托給相應的AuthenticationStrategy進行多Realm身份驗證,預設
ModularRealmAuthenticator會調用AuthenticationStrategy進行多Realm身份驗證;
5、Authenticator 會把相應的token 傳入Realm,從Realm 擷取身份驗證資訊,如果沒有返
回/抛出異常表示身份驗證失敗了。此處可以配置多個Realm,将按照相應的順序及政策進
行通路。
6.1.1、Realm
域,Shiro 從Realm擷取安全資料(如使用者、角色、權限),就是說SecurityManager
要驗證使用者身份,那它需要從Realm擷取相應的使用者進行比較以确定使用者身份是否合法;
也需要從Realm得到使用者相應的角色/權限進行驗證使用者是否能進行操作;可以把Realm看
成DataSource,即安全資料源。
可自定義單 Realm配置:
#聲明一個realm
myRealm1=com.github.zhangkaitao.shiro.chapter2.realm.MyRealm1
#指定securityManager的realms實作
securityManager.realms=$myRealm1
自定義Realm實作:
public class MyRealm1 implements Realm {
@Override
public String getName() {
return "myrealm1";
}
@Override
public boolean supports(AuthenticationToken token) {
//僅支援UsernamePasswordToken 類型的Token
return token instanceof UsernamePasswordToken;
}
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws
AuthenticationException {
String username = (String)token.getPrincipal(); //得到使用者名
String password = new String((char[])token.getCredentials()); //得到密碼
if(!"zhang".equals(username)) {
throw new UnknownAccountException(); //如果使用者名錯誤
}
if(!"123".equals(password)) {
throw new IncorrectCredentialsException(); //如果密碼錯誤
}
//如果身份認證驗證成功,傳回一個AuthenticationInfo實作;
return new SimpleAuthenticationInfo(username, password, getName());
}
}
7、授權
授權也叫通路控制,即在應用中控制誰能通路哪些資源(如通路頁面/編輯資料/頁面操作等)。在授權中需了解的幾個關鍵對象:主體(Subject)、資源(Resource)、權限(Permission)、角色(Role)。
主體即通路應用的使用者,在Shiro中使用Subject代表該使用者。使用者隻有授權後才允許訪
問相應的資源。
資源在應用中使用者可以通路的任何東西,比如通路JSP 頁面、檢視/編輯某些資料、通路某個業務方法、列印文本等等都是資源。使用者隻要授權後才能通路。
權限安全政策中的原子授權機關,通過權限我們可以表示在應用中使用者有沒有操作某個資源的權力。即權限表示在應用中使用者能不能通路某個資源,如:通路使用者清單頁面,
檢視/新增/修改/删除使用者資料(即很多時候都是CRUD(增查改删)式權限控制),列印文檔等等。
角色代表了操作集合,可以了解為權限的集合,一般情況下我們會賦予使用者角色而不是權限,即這樣使用者可以擁有一組權限,賦予權限時比較友善。典型的如:項目經理、技術總監、CTO、開發工程師等都是角色,不同的角色擁有一組不同的權限。
7.1 授權方式
程式設計式:通過寫if/else 授權代碼塊完成,Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {….}
注解式:通過在執行的Java方法上放置相應的注解完成@RequiresRoles("admin")
public void hello() {….}
JSP/GSP 标簽:在JSP/GSP 頁面通過相應的标簽完成
<shiro:hasRole name="admin"><!— 有權限—></shiro:hasRole>
7.2 授權
1)基于角色的通路控制(隐式角色)
ini配置檔案配置使用者擁有的角色(shiro-role.ini):
[users]
zhang=123,role1,role2
wang=123,role1
實作:
Subject subject = SecurityUtils.getSubject();
subject().hasAllRoles(Arrays.asList("role1", "role2")));
2)基于資源的通路控制(顯示角色)
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傳回相應的權限資訊。
實作:
Subject subject = SecurityUtils.getSubject();
subject().isPermitted("user:create");
subject().isPermittedAll("user:update", "user:delete");
7.3 Permission
字元串通配符權限
規則:“資源辨別符:操作:對象執行個體ID” 即對哪個資源的哪個執行個體可以進行什麼操作。其預設支援通配符權限字元串,“:”表示資源/操作/執行個體的分割;“,”表示操作的分割;“*”表示任意資源/操作/執行個體。
資源權限判斷
subject().checkPermissions("system:user:update", "system:user:delete");
權限字元串缺失部分的處理
如“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”,即字尾比對必須指定字首(多個冒号就需要多個*來比對)。
性能問題
通配符比對方式比字元串相等比對來說是更複雜的,是以需要花費更長時間,但是一般系統的權限不會太多,且可以配合緩存來提供其性能,如果這樣性能還達不到要求我們可以實作位操作算法實作性能更好的權限比對。另外執行個體級别的權限驗證如果資料量太大也不建議使用,可能造成查詢權限及比對變慢。可以考慮比如在sql 查詢時加上權限字元串之類的方式在查詢時就完成了權限比對。
7.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比對流程:
1、首先檢查相應的Realm是否實作了實作了Authorizer;
2、如果實作了Authorizer,那麼接着調用其相應的isPermitted*/hasRole*接口進行比對;
3、如果有一個Realm比對那麼将傳回true,否則傳回false。
如果Realm進行授權的話,應該繼承AuthorizingRealm,其流程是:
3.1.1、如果調用hasRole*,則直接擷取AuthorizationInfo.getRoles()與傳入的角色比較即可;
3.1.2、首先如果調用如isPermitted(“user:view”),首先通過PermissionResolver 将權限字元串轉換成相應的Permission 執行個體,預設使用WildcardPermissionResolver,即轉換為通配符的WildcardPermission;
2 、通過AuthorizationInfo.getObjectPermissions() 得到Permission 執行個體集合;通過
AuthorizationInfo. getStringPermissions()得到字元串集合并通過PermissionResolver 解析為
Permission 執行個體;然後擷取使用者的角色,并通過RolePermissionResolver 解析角色對應的權
限集合(預設沒有實作,可以自己提供);
3、接着調用Permission. implies(Permission p)逐個與傳入的權限比較,如果有比對的則傳回
true,否則false。
7.4.1、Realm
可自定義單 Realm配置:
#聲明一個realm
myRealm1=com.github.zhangkaitao.shiro.chapter2.realm.MyRealm1
#指定securityManager的realms實作
securityManager.realms=$myRealm1
自定義Realm實作:
public class MyRealm1 implements Realm {
@Override
public String getName() {
return "myrealm1";
}
@Override
public boolean supports(AuthenticationToken token) {
//僅支援UsernamePasswordToken 類型的Token
return token instanceof UsernamePasswordToken;
}
@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:*"));
}
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws
AuthenticationException {
….代碼見6.1.1,省略
}
}