Shiro 是 JAVA 世界中新近出現的權限框架,較之 JAAS 和 Spring Security,Shiro 在保持強大功能的同時,還在簡單性和靈活性方面擁有巨大優勢。本文就帶領讀者一睹 Shiro 的風采。
可能大家早先會見過 J-security,這個是 Shiro 的前身。在 2009 年 3 月初之前,這個安全框架叫做 J-security,由於某些原因,更名爲 Shiro(或者 Ki,意爲 Fortress),是 Apache 的孵化項目,鑑於本文編寫時 Shiro 的還沒有正式發佈的版本,本文使用的是 Jsecurity 的穩定版本 0.9,本文中 Shiro 等同於 Jsecurity。
本文將涉及 Shiro 的整體框架、安全模型、關鍵概念類,同時給出了 Shiro 以及 Grails Shiro Plugin 的使用示例,可以下載文中使用的源代碼。
本文代碼的開發環境:
- Jsecurity 0.9
- Grails 1.2.0
- Grails Shiro Plugin 1.0.1
- SpringSource Tool Suite 2.3
Shiro 是一個強大而靈活的開源安全框架,能夠非常清晰的處理認證、授權、管理會話以及密碼加密。如下是它所具有的特點:
- 易於理解的 Java Security API;
- 簡單的身份認證(登錄),支持多種數據源(LDAP,JDBC,Kerberos,ActiveDirectory 等);
- 對角色的簡單的籤權(訪問控制),支持細粒度的籤權;
- 支持一級緩存,以提升應用程序的性能;
- 內置的基於 POJO 企業會話管理,適用於 Web 以及非 Web 的環境;
- 異構客戶端會話訪問;
- 非常簡單的加密 API;
- 不跟任何的框架或者容器捆綁,可以獨立運行。
目前還有其他出現較早的安全框架,比如 JAAS,Spring Security。
JAAS —面世的時間最早,但是鑑於其在使用上有很大的限制,很少有人真正的使用它。可以說它不是一個好的應用程序級別的安全框架;
Spring Security —目前是 Java 安全框架領域當之無愧的老大,已經非常成熟了;如果使用 Spring 框架,可以首選 Spring Security,但是對於單應用來說,Shiro 更顯簡單方便。
下面就開始我們的 Shiro 之旅吧!
首先,我們來看看的 Shiro 的整體架構,見下圖:
從上圖可以看出,Shiro 主要有四個組件:
-
SecurityManager
典型的 Facade,Shiro 通過它對外提供安全管理的各種服務。
-
Authenticator
對“Who are you ?”進行覈實。通常涉及用戶名和密碼。
這個組件負責收集 principals 和 credentials,並將它們提交給應用系統。如果提交的 credentials 跟應用系統中提供的 credentials 吻合,就能夠繼續訪問,否則需要重新提交 principals 和 credentials,或者直接終止訪問。
-
Authorizer
身份份驗證通過後,由這個組件對登錄人員進行訪問控制的篩查,比如“who can do what”, 或者“who can do which actions”。Shiro 採用“基於 Realm”的方法,即用戶(又稱 Subject)、用戶組、角色和 permission 的聚合體。
-
Session Manager
這個組件保證了異構客戶端的訪問,配置簡單。它是基於 POJO/J2SE 的,不跟任何的客戶端或者協議綁定。
Shiro 的認證和籤權可以通過 JDBC、LDAP 或者 Active Directory 來訪問數據庫、目錄服務器或者 Active Directory 中的人員以及認證 / 籤權信息。SessionManager 通過會話 DAO 可以將會話保存在 cache 中,或者固化到數據庫或文件系統中。
從 Shiro 的框架圖,已經能夠體會到這個工具的簡單了。下面讓我們來看看 Shiro 是如何工作的。先了解一下它的安全模型吧!見下圖:
上圖中,涉及了 Shiro 的五個概念:
- Subject 是安全領域術語,除了代表人,它還可以是應用。在單應用中,可將其視爲 User 的同義詞。
- Principal 是 Subject 的標識,一般情況下是唯一標識,比如用戶名。
- Role 和 Permission 分別代表了不同粒度的權限,從上圖中可以看出 Role 的粒度更大些,Permission 代表了系統的原子權限,比如數據的修改、刪除權限。對於簡單的權限應用,可以不需要 Permission。
- Realm 是一個執行者,負責真正的認證和鑑權。
實現應用的安全模塊的關鍵在於:定義合適的 role 和 permission,這就需要遵循如下原則:
- role 沒有實質內容,只是代表一組 permission,目的是爲了管理的方便,一般都是動態定義;
- permission 一般都是預先定義好的,不允許動態改變,除非源代碼改動,它纔會變化,它是整個安全模塊的基礎;
- 要使 permission 也能動態定義,並非不可能,但是這將使鑑權非常複雜,甚至可能導致鑑權語句遍佈整個程序,得不償失;
- 當然有一個例外:如果知道 permission 動態定義的規則和鑑權規則,如 Grails 的 fileter 中“${controllerName}:${actionName}:${params.id}”也可實現 permission 的動態定義
理解 Shiro 的架構和安全模型了,我們來看看更具體些的內容。下圖顯示了 Shiro 中的關鍵概念類(參考資料 -- JSecurity Mini Guide)。
AuthenticationToken 和 AuthenticationInfo
前者在認證前使用,描述認證所需的信息,最常用的就是 username 和 password 對;後者在認證後使用,內容同前,但是表示已經經過認證的信息。
RememberMe
代表的是一種可能狀態,並不表示該 Subject 已經經過了認證。對於一些普通的操作,這種可能狀態並無大礙,但一旦涉及安全敏感的操作,必須經過認證。
Credentials 和 CredentialsMatcher
Credentials 是 Subject 的證書,在認證時使用,最常用的就是 password。在通常情況下,爲了安全起見,Subject 的 credentials 都需要加密保存,於是 CredentialsMatcher 的作用就體現出來了,見下圖:
這裏 CredentialsMatcher 需要將加密後的證書跟用戶登錄時提供的證書進行比對,完成認證的過程。
PAM= Pluggable Authentication Modules
在有多個 Realm 的時候使用。由認證策略決定認證結果,即 PAM= Relams + 認證策略。一般的策略有 3 種:AllSuccessful、AtLeastOneSuccessful 和 FirstSuccessful。
AuthorizationInfo
可以看成是 Role + Permission 的組合體。
PermissionResolver 和 Permission
它們之間的關係如下:
圖
5. PermissionResolver 和 Permission 的關係
在 Shiro 中,權限被轉化爲一種字符串描述(字符串分級表示,稱之爲 WildcardPermission),從而將權限轉化爲類似於對象 equals 的操作(Shiro 中的 implies 方法)。
內置的權限有 2 個:
- AllPermission,總是返回 true
- WildcardPermission,權限字符串的表示方式。
這裏重點聲明一下。WildcardPermission 是 Shiro 的精妙之處,我們可以將權限表示成字符串,這樣對權限的控制可以不拘泥於物理存儲,比如對 messagge 類具有修改和刪除權限可以標識爲:message:update,delete:*,其中‘ * ’表示所有;第一級分隔符爲‘ : ’;第二級分隔符爲‘ , ’,而對於權限字符串的解釋完全可以由應用自己來定。
如果要比較權限字符串,可以使用 permission1.implies(permission2),它分別比較對應位置的字符串,在如下情況中,結果會返回 true:
- permission1 中的子串有 * 或 permission1 子串 ==permission2 子串;
- permission1 無子串,permission2 有;
- permission1 有子串,permission2 無,permission1 的所有子串都是 *。
總的說來,Shiro 中的 Permission 需要注意如下內容:
- 權限的比較實際是字符串的比較,只不過是考慮到了字符串的分級
- 字符串的分級劃分完全由使用者自己決定,Shiro 的慣例是 3 級:資源 : 操作 : 實例。
- 字符串的使用必須一致,分隔符之間不要有空格,避免無意間引入的不一致。如:定義使用“file : create, update : 1”,而驗證使用“file : update”,那麼分解之後一個是“ update ”,一個是“ update”,因空格而引起不等。
Realm
這是一個實際訪問安全實體的組件,一般是應用相關的,跟數據源的關係是 1-1。它負責完成認證和鑑權,getAuthenticationInfo 代表了 login 的嘗試,鑑權方法則由 Authorizer 繼承而來。此處也體現了 Shiro 代碼的另一個特點,通過繼承來擴充功能。以常用的 JdbcRealm 爲例,其繼承鏈如下:
Session
它關聯一個 Subject 的上下文,其作用類似於在 HttpSession 中保存用戶標識,session 一旦過期,則重新登錄。Shiro 的 Session 是獨立的,其目的是做到環境無關性。爲了利用 Web 環境中,Shiro 實現了一個直接使用 HttpSession 的 WebSession。
SecurityManager
這是一個 Façade 接口,=Authenticator + Authorizer + SessionFactory。在整體框架圖中已經看到了它在 Shiro 中所處的位置。其特點同 Realm,一樣是使用繼承不斷地擴充功能。對於 Web 應用一般使用 DefaultWebSecurityManager。
Filter
在 Web 環境下使用 filter 進行認證和權限檢查是毋庸置疑的,而 Shiro 的特點則在於由一個主 Filter 將一羣子 filter 串起來:
在實際使用時,須注意:
- web.xml 中只需配置 JSecurityFilter。對於 Spring 應用,則使用 SpringJSecurityFilter;
-
子 filter 作爲主 filter 的配置參數值出現,特點是:順序相關
-
- 對於多個 URL,驗證順序是由上至下,類似 Exception 的匹配。因此,使用順序應該是由細到粗。
- 對於同一 URL,子 filter 的驗證順序是從左至右的 AND 操作。
-
- 如果配置值中含有分隔符,如 Permission,就需要使用引號來轉義。
Subject
subject 代表了一個用戶的狀態和操作,它提供了所有安全相關的操作,包括認證和籤權。可以將其視爲另一種形式的 Façade。缺省實現是將這些操作委派給其內部包含的 SecurityManager。
Configuration
configuration 負責將所有這些組件串起來,最終創建 SecurityManager。在 Shiro 中,缺省格式是 ini。整個配置關係如下圖:
其中:
- JSecurityFilter 創建 Configuration 實例,並將 ini 參數值傳給 Configuation。在 Spring 環境中,分別使用 SpringJSecurityFilter 和 SpringIniWebConfiguration。
- Configuration 實際就是 SecurityManager 的 Factroy,對 SpringIniWebConfiguration 而言,它需要知道 SecurityManager 的 BeanName,該值由 SpringJSecurityFilter 的初始化參數“securityManagerBeanName”值決定。即 SpringJSecurityFilter,實際有兩個初始化參數:
-
- config,是 ini 配置文件內容
- securityManagerBeanName,是 SecurityManager 的 BeanName
SecurityUtils
這是 Shiro 中最重要的工具類,由它可以方便地獲得 Subject 和 SecurityManager。
雜項
- AOP,提供 AOP 方面的支持,實現對某個類某個方法的攔截,從而使權限控制延伸至類的方法。
- Cache,提供緩存支持
- Codec,提供編碼方面的支持
- Crypto,提供加密支持
- IO,從多個資源位置讀寫原始數據
- JNDI,提供 jndi 支持
- util,工具類支持
- 標籤類,用於 Web 頁面
對 Shiro 有了一個感官認識後,下面我們就親自動手試試這個框架吧!下面給大家舉了兩個使用案例。
在開始案例的學習之前,先作好準備工作 -- 獲得 Shiro 相關的 jar 包,獲取途徑有兩種:
- 直接到 J-security 的網站上 下 載,本文用到的就是這個;
-
由於 Shiro 目前是 Apache 的孵化項目,還沒有發佈正式的版本,但是我們可以到 Subversion 上下載代碼,之後使用 Maven 構建
mkdir shiro
cd shiro
svn co https://svn.apache.org/repos/asf/incubator/shiro/trunk/
mvn install
之後會得到 shiro-all-1.0-incubating-SNAPSHOT.jar,就可以使用 Shiro 了。
這個案例中,我們使用 Grails 向大家講述 Shiro 的使用。我們要實現如下功能:
- 用戶登錄後方可進入系統;
- 假定一個 message 的安全內容,用戶可以創建 message 的內容,但是如果需要修改 / 刪除 message 的內容就必須具有相應的權限;
- Admin 具有所有的權限;
- message 的權限跟角色關聯。
示例程序執行的流程如下:
從上圖中可以看到,任何人要訪問應用中受保護的 URL,首先要通過 Filter 檢查用戶是否經過認證;對於沒有認證的用戶會將訪問定向到登錄頁面;對於已經認證的用戶,會對用戶進行鑑權,這個用戶是否具有訪問其所提交的 URL 的權限;而管理員可以給角色授權。
好了,開始程序的編寫啦!
創建安全領域類
最常見的就是 User、Role 和 Permission,見清單 1。
清單
1. User/Role/Permission 的 Domain class
|
這裏使用了最簡單的情形,即權限傳遞結構爲:Permission -> Role -> User。通常情況下,Permission 也可以分配給單個 User。
創建一個安全實體
實體名爲 message,見清單 2。只有經過授權的用戶才能對這個實體進行修改和刪除。
|
配置 web.xml
清單
3. 在 web.xml 加入 SecurityFilter 的內容:
|
這裏需要注意:
- 這個 Filter 應該在 Grails 的 web.xml 中所有缺省的 Filter 最後;
- url-pattern 不要使用“/**”,因爲這樣會造成登錄頁的 css 和圖片無法訪問。解決辦法,可以通過遵循“只能通過 Controller/Action 訪問”這個規則,並使用 Grails 的 Filter 機制,可以保證所有安全 URL 不被非法訪問。
創建 realm
清單
4. conf/spring/resources.groovy
|
這裏使用了 JdbcRealm,同時根據應用情況修改了相應的 SQL。如果允許 Permission 直
接分配給 User,即 Permission 和 User 之間是多對多關係,那麼 permissionsQuery 應該使用 union,即“role 相關 permission union user 相關 permission”。對於 User 有多個 Role 的情況,JdbcRealm 會循環得出總的結果。
安全守護神:SecurityFilters
下面就是我們的安全守護神:SecurityFilters,這裏遵循 Grails 的 Filter 語法。見清單 5。
|
代碼中 :
- authc 表示的是所有用戶對應用系統的任何訪問都要經過 auth 認證,對於沒有認證的用戶的訪問會重定向到登錄界面;
- admin 表示的是屬於 admin 角色的用戶對 user/role/permission 具有所有權限,對於非 admin 角色的用戶會對其提示沒有 user/role/permission 的訪問權限;
- editmessage 表示當用戶對 message 進行修改或者刪除的時候對其進行鑑權,只有具有 message 的 update/delete 權限的用戶才能對 message 進行修改。
在上述代碼中還可以看出,我們通常可以由 SecurityUtils 爲出發點獲得 Subject 和 SecurityManager。需要注意的是,認證的 Filter(authc)要打頭陣。
認證的代碼
|
授權部分很簡單,即對安全實體進行 CRUD。其中 Permission 的權限字符串根據實際情況形成,在本例中:
-
-
- 如果對所有 message 具有修改權限,權限字符串可以爲:message:delete,update:*;
- 如果針對某一個 message 具有修改權限,權限字符串可以爲:message:update,delete:messageid。
-
在示例一中,所有的代碼都是自己手動寫的,這就對初學者要求有些高了。但可喜的是 Grails 社區有了 Shiro 的 plugin,讓我們的工作變得非常簡單。同樣示例一的功能,看看 plugin 能夠給我們帶來什麼樣的驚喜?
使用步驟如下:
-
安裝 Shiro Plugin,在你的 grails 項目中運行:
grails install-plugin shiro
, 會創建 grails-app/realms 目錄,並提供如下新的 Grails 命令:-
grails create-auth-controller
,創建 AuthController 以及登錄窗口,Controller 提供了登錄、登出和權限驗證失敗處理等 Action。 -
grails create-db-realm
,創建一個訪問數據庫的 Realm -
grails create-ldap-realm
,創建一個訪問 ldap 的 Realm -
grails create-wildcard-realm
,創建一個訪問數據庫的 Realm,但使用的是 Shiro 的 WildcardPermission。 -
grails quick-start
,它是 create-auth-controller 和 create-db-realm 的集合體,是 Shiro 的快速入門,在接下來的內容中將詳細介紹它的使用。
-
-
下面進行 Shiro 快速入門。在 grails 項目的目錄下執行:
grails quick-start
,這個命令會創 建如下內容:- 在 grails-app/realms 下創建 ShiroDbRealm,這個 realm 是訪問控制的信息庫,用來決定一個用戶有權訪問哪些內容;
- 在 grails-app/domain 創建 ShiroRole.groovy 和 ShiroUser.groovy;
- 在 grails-app/controllers 下創建 AuthController,這裏提供了登錄和退出的 action;
- 在 grails-app/views 下創建 auth 文件夾以及 login.gsp;
- 在 grails-app/conf/ 下創建 SecurityFilters.groovy,這裏管理着對所有 controller 和 action 的訪問控制。
- 啓動程序,當訪問 controller 的時候,頁面就會重定向到登錄頁面。但是這個時候是無法登錄,因爲我們沒有添加用戶。
-
進入到 grails-app/conf/ 修改 BootStrap.groovy,
清單 7. 修改後的 BootStrap.groovy
def init = { servletContext ->
def user = new User(username: "user",
passwordHash: new Sha1Hash("user").toHex())
user.save()
def role= new Role(name:"admin").addToUsers(user).save()
}重新啓動程序,就能使用 user/user 登陸了。
-
在 plugin 缺省創建的 SecurityFilters 中使用了對所有 URL 進行 before 攔截,但是我們根據實際情況進行修改,例如我們要示例一的 filter 內容,就可以做如下更改:
清單 8. 更改後的 SecurityFilters
auth(controller: "*", action: "*"){
before={
accessControl{true}
}
}
management(controller: "user|role|permission", action: "*"){
before={
accessControl{role("admin")}
}
}
message(controller: "message", action: "delete|update"){
before={
accessControl{
permission("message:${actionName}:${params.id}")
}
}
}看到這裏,讀者可能已經注意到了,這裏的代碼比示例一中的 Filter 的代碼簡單的許多。對,Shiro 插件已經將示例一中的類似代碼封裝到 ShiroGrailsPlugin.groovy 中了,我們使用的時候只需要:
調用 accessControl 方法,該方法的參數可以是 Map、Closure 或者 Map+Closure;
使用 role( …… ),驗證訪問對象是否具有相應的角色;
使用 permission( …… ),驗證訪問對象是否具有相應的 Permission。
- 授權部分內容參見上一示例,這裏不做冗述。
-
Shiro Plugin 中除了爲我們提供了上述方便之外,還提供了一些常用的 taglib 來增強用戶界面 , 這些 taglib 都在 pluginPath/grails-app/taglib /ShiroTagLib.groovy 中定義,我們可以直接在 gsp 中使用它們。
比如,只有具有修改權限的用戶才能夠看到一些修改類的按鈕顯示,可以這樣寫:
清單 9. Taglib 的使用
<shiro:hasPermission permission="message:update,delete">
<span class="button">
<g:actionSubmit class="edit" value="Edit" />
</span>
<span class="button">
<g:actionSubmit class="delete"
οnclick="return confirm('Are you sure?');"
value="Delete" />
</span>
</shiro:hasPermission>如下是經常使用到的 Tag:
- principal, 輸出當前用戶的標識
- hasRole,判斷當前用戶是否屬於給定的角色,參數:name
- hasPermission, 判斷當前用戶是否具有指定的權限,參數:type,action 或者 permission
- isLoggedIn, 判斷當前用戶是否已經登錄
- hasAnyRole,判斷當前用戶是否屬於給定的某個角色,參數:in
更多的 Tag 請參考 Shiro Plugin 的 gapi 的文檔。
如果您已經非常瞭解了 Shiro,可以採用示例一的方式自己寫所有的代碼,但是對於初學者,作者還建議使用 Shiro plugin,這個插件幫助我們生成了安全相關的基本內容,以及提供了方便的 Tag。
讀到這裏,是不是覺得 Shiro 真的不錯?!
這裏給大家舉的示例只是拋磚引玉。Shiro 真正的精髓還需要在項目中慢慢的體會。本文是引領我們走向 Shiro 的第一步。在這裏要感謝胡鍵對本文編寫的大力支持。
源代碼中,shiroApp 是示例一的源代碼,ShiroPlugin 是示例二的源代碼。