在最高概念層面,Shiro的架構包含了3個主要概念:Subject, SecurityManager 和 Realms. 下面的圖表是這些組件如何交互的高級概述,我們將覆蓋下面的每一個概念:
"User”往往意味著一個人類,一個Subject可以是一個人,但它也可能代表一個第三方服務,后臺賬戶,cron作業,或任何類似的東西-基本上是當前與軟件交互的任何東西.
Subject 實例都綁定在SecurityManager上. 當你與Subject交互時,這些交互都會轉換成特定subject與SecurityManager的交互.
SecurityManager 是Shiro架構的心臟,它扮演了一種保護傘(umbrella)對象,協調其內部的安全組件共同形成一個對象圖.
后面我們會詳細討論SecurityManager, 但要意識到,當你與Subject交互時,其實是幕后的SecurityManager在執行Subject的安全操作.這可以從上面的圖中得到反映.
Realms扮演的是Shiro與應用程序安全數據的橋梁或連接器.
Shiro提供了多個開箱即用的Realms來連接多種安全數據源,如LDAP, 關系型數據庫(JDBC),像INI和屬性文件這樣的文本配置源.
下圖展示了Shiro的核心架構概念以及每種概念的簡短概述:

與軟件進行交互的實體(用戶,第三方服務,cron作業等等)特定安全視圖.
正如上面提到的, SecurityManager是Shiro架構的心臟.它主要充當保護傘對象,協調其管理組件來確保工作順利進行. 同時,它也會管理每個應用程序用戶的Shiro視圖,因此它知道如何執行每個用戶上的操作.
認證負責驗證用戶的登錄. 當一個用戶試圖登錄時,認證將執行驗證邏輯. 認證組件知道如何協調一個或多個存儲相關用戶信息的. 從Realms中獲取的數據用于驗證用戶的身份,以確保其合法性.
如果配置了多個Realm, AuthenticationStrategy 會協調Realms來確定在哪些情況下認證是成功或是失敗的(例如,如果某個realm是成功的,但其它是失敗的l,那么認證是成功的嗎?
是否需要所有realms都成功?還是只需要第一個成功?).
授權是在應用程序中負責確定訪問控制的組件.最終說來它只是允許用戶做什么或不做什么的機制.類似于Authenticator,Authorizer也知道如何協調多個后端數據源來訪問角色和權限信息.
Authorizer可使用這些信息來決定一個用戶是否允許來執行給定的操作.
SessionManager知道如何來創建和管理用戶生命周期,以為所有環境中的用戶提供強大的會話體驗. 在安全框架的世界中,這是獨特的特性- Shiro有管理任何環境中用戶會話的能力, 即使這些環境處于非Web/Servlet或在EJB容器環境.
默認情況下,如果可行的話,Shiro會使用現有的會話機制, (例如. Servlet容器),但如果不存在這樣的環境,如獨立程序或非web環境,它會使用內置的企業會話管理來提供相同的編程體驗. SessionDAO的存在允許使用任何數據源來存儲會話.
SessionDAO可代表SessionManager來執行會話持久化操作(CRUD).這允許在會話管理框架中插入任何數據存儲.
緩存管理器用于創建和管理其它Shiro組件使用的緩存實例生命周期.因為Shiro在執行認證,授權和會話管理時,可訪問多個后端數據源,當使用這些數據源時,緩存通常是框架中提高性能的最好架構特征.
任何一個現代開源或企業級緩存產品都可以插入到Shiro中,以提供快速,高效的用戶體驗.
加密是企業級安全框架中一個很自然的功能. Shiro的加密包包含了易于使用和理解的加密 Ciphers, Hashes (aka digests)表現以及不同的編碼實現.
包中的所有類都是精心設計為易于使用和理解. 任何使用過Java本地加密的人,都可以輕易地駕馭它.
Shiro的加密APIs簡化了復雜的Java機制,使得加密對于普通人來說也能簡單使用.
正如上面所提到的, Realms扮演的是Shiro與應用程序安全數據之間的橋梁或連接器.
當它與像用戶帳戶這樣的相關安全數據進行實際交互時,如執行認證(login) 和授權(訪問控制), Shiro會查看為應用程序中配置的一個或多個Realms.
你可以根據需要來配置多個Realms (通常情況下一個數據源一個Realms) ,Shiro會按照需要來為認證和授權協調數據.
SecurityManager
因為Shiro的API鼓勵使用以Subject為中心的編程模型,大部分應用程序開發者會極少直接與SecurityManager交互(框架開發者有時可能會發現它很有用).
即使是這樣,了解SecurityManager的功能,特別是在為應用配置時, 也是極其重要的.
設計
如前所述,應用程序安全管理器會為所有應用程序用戶執行安全操作和管理用戶狀態. 在Shiro的默認SecurityManager實現中包含:
- Authentication(認證)
- Authorization(授權)
- Session Management(會話管理)
- Cache Management(緩存管理)
- Realm coordination(Realm協作)
- Event propagation(事件傳播)
- "Remember Me" Services(記住我服務)
- Subject creation(Subject創建)
- Logout(登出)
等等.
但在單個組件中,管理著多個功能. 而且,如果一切功能都集中到單一的實現類中,會使這些操作變得靈活和可定制化是相當困難的.
為了簡化配置和開啟靈活的配置性/可插拔性, Shiro的實現設計上是高度模塊化的- SecurityManager實現及其類層次并沒有做這么多工作.
相反,SecurityManager實現幾乎充當的是輕量級容器組件, 幾乎把所有行為/操作都委派給了內嵌/包裝組件.上面的詳細框架圖就反映了這種包裝設計.
雖然是由組件來實際執行邏輯,SecurityManager實現知道如何以及何時來協調組件來完成正確行為.
SecurityManager 實現同時也是JavaBeans兼容的,這允許你通過標準的Java訪問器/修改器方法(get*/set*)來輕松地定制可插拔組件.
這意味著Shiro的架構模塊可以很容易配置自定義行為。
簡單配置
由于JavaBeans的兼容性, 使其很容易地通過支持JavaBeans的自定義組件來配置SecurityManager,如Spring, Guice, JBoss等等. Apache Shiro 配置
Shiro設計為可工作于任何環境, 從簡單的命令行應用程序到大型企業集群應用程序. 由于環境的多樣性,存在著多種配置機制. 本章講述Shiro核心支持的配置機制.
多個配置選項
Shiro的SecurityManager實現以及所有支持組件都是與JavaBeans兼容的.這允許Shiro使用特定配置格式,如常規Java, XML (Spring, JBoss, Guice, etc), YAML, JSON, Groovy Builder標記來配置. 編程配置
創建SecurityManager并使用可用的最簡單方式是創建一個org.apache.shiro.mgt.DefaultSecurityManager. 例如:
Realm realm = //實例化或獲取一個Realm實例.我們后面會討論Realms. SecurityManager securityManager = new DefaultSecurityManager(realm); //通過靜態內存,使SecurityManager 實例對整個應用程序可用: SecurityUtils.setSecurityManager(securityManager);
令人驚訝的是,雖只有3行代碼,但你現在有了一個適合于許多應用且功能齊全的Shiro環境。多么容易!
SecurityManager 對象圖
正如在
Architecture章節中討論的, Shiro的SecurityManager實現本質上內嵌特定安全的組件模塊對象圖.
因為它們也是JavaBeans兼容的,你可調用任何內嵌組件上的 getter和setter方法來配置SecurityManager和它的內部對象圖.
例如,如果你想配置SecurityManager實例來使用自定義的SessionDAO來定制
Session Management,你可以直接使用內置的SessionManager的setSessionDAO來設置SessionDAO:
...
DefaultSecurityManager securityManager = new DefaultSecurityManager(realm);
SessionDAO sessionDAO = new CustomSessionDAO();
((DefaultSessionManager)securityManager.getSessionManager()).setSessionDAO(sessionDAO); ...
使用直接方法調用,你可以配置SecurityManager的對象圖的任何部分.
雖像程序化定制一樣簡單,但它并不代表大多數現實世界應用程序的理想配置。
有以下幾個原因為什么編程配置可能不適合您的應用程序:
- 它需要你了解和實例化一個直接實現.如果不需要了解具體和查找位置,這是很好的.
- 由于Java的類型安全特性,你需要在get*方法中對特定實現進行類型轉換. 因此太多的轉換是丑陋的,冗長的,并緊密耦合了你的實現類.
- SecurityUtils.setSecurityManager 方法調用使得實例化了的SecurityManager實例化了一個 VM 靜態單例, 在這里是很好的,但對于多數應用程序來說,如果在同一個JVM中運行了多個開啟了Shiro的應用程序,將會引發問題.本身來說,如果實例是應用程序級單例的,這會更好,但不是靜態內存引用.
- 為了讓Shiro配置生效,每次你都需要重新編譯代碼.
但,即使有這些警告,直接編程操作方案對于內存受限的環境,仍然是價值的,像智能手機應用程序.如果你的應用程序不會運行內存受限環境中,你會發現基于文本的配置更加簡單和閱讀.
INI 配置
大部分應用程序都受益于基于文本的配置,這樣可以獨立于源碼進行修改,甚至使得那于不熟悉Shiro API的人來說,也更易于理解.
為了確保通用基于文本配置的解決方案能工作于任何環境(有很小的依賴),Shiro支持使用
INI format 來構建SecurityManager 對象圖和它的支撐組件.
INI易于閱讀和配置,并可以簡單建立而適用于大部分應用程序.
從INI創建SecurityManager
下面有兩個例子來講述如何基于INI配置來構建SecurityManager.
從INI資源來構建SecurityManager
我們可以從INI資源路徑來創建SecurityManager實例. 資源可通過文件系統, classpath,或URLs來獲取,此時其前輟分別為file:, classpath:, 或 url: .
這個例子使用Factory來從classpath根路徑下來攝取shiro.ini文件,并返回SecurityManager實例:
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.util.Factory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.config.IniSecurityManagerFactory;
...
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
從INI實例來構建SecurityManager
For example:
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.util.Factory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.config.Ini;
import org.apache.shiro.config.IniSecurityManagerFactory;
...
Ini ini = new Ini(); //按需填充 Ini實例
...
Factory<SecurityManager> factory = new IniSecurityManagerFactory(ini);
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
現在,我們知道如何從INI配置來構建一個SecurityManager, 讓我們明確地了解如何定義一個Shiro INI配置.
INI 分段
INI是一個包含由唯一分段組織的key/value對的文本配置. 每個段中Keys是唯一的,而不是整個配置(這一點不像JDK Properties). 每個段都可像單個Properties定義進行查看.
注釋行可以#或;開頭
下面是分段的例子:
# ======================= # Shiro INI configuration # =======================
[main]
# 這里用于定義對象及其屬性,如securityManager, Realms 以及任何需要用于構建SecurityManager的配置
[users]
# 'users' 分段用于簡單部署
# 當你只需要少量靜態定義的用戶帳戶時
[roles]
# 'roles' 分段用于簡單部署
# 當你只需要少量靜態定義的角色時
[urls] # 'urls' 分段用于web應用程序中作基于url的基礎安全控制
# 我們會Web文檔中討論這個分段
[main]
[main]分段是你配置應用程序SecurityManager實例,及其任何依賴,如
Realms.
像SecurityManager或其依賴這樣的配置對象實例聽起來對于INI來說,是一件困難的事情,因為在這里我們只能使用name/value對.
但是通過一點點的約定和對對象圖的理解,你會發現你可以做得很好。Shiro用這些假設使一個簡單的配置機制成為了可能。
我們常喜歡把這種方法稱為“窮人的“依賴注入,雖然不比全面的Spring/ Guice / JBoss的XML文件強大,但你會發現它沒有過多的復雜性。
當然也可使用其他的配置機制,但對于Shiro來說,他們不是必須的.
只是為了刺激你的欲望,這里有一個有效的[main]配置實例:
[main]
sha256Matcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
myRealm = com.company.security.shiro.DatabaseRealm
myRealm.connectionTimeout = 30000
myRealm.username = jsmith
myRealm.password = secret
myRealm.credentialsMatcher = $sha256Matcher
securityManager.sessionManager.globalSessionTimeout = 1800000
定義對象
細想下面的[main] 分段片斷:
[main] myRealm = com.company.shiro.realm.MyRealm ...
這行實例化了一個com.company.shiro.realm.MyRealm類型的新對象,并為了以后的引用和配置,使其在名稱myRealm名稱下可用.
如果實例的對象實現了org.apache.shiro.util.Nameable接口, 那么可在對象上使用名稱值(myRealm)來調用Nameable.setName.
設置對象屬性
原始類型值
簡單原始類型屬性可以使用=號來賦值:
...
myRealm.connectionTimeout = 30000
myRealm.username = jsmith ...
這些行的配置轉換成方法調用為:
...
myRealm.setConnectionTimeout(30000);
myRealm.setUsername("jsmith"); ...
這怎么可能呢?它假定所有的對象都是java bean兼容的POJOs。
在后臺,Shiro默認使用Apache Commons BeanUtils來設置這些屬性。所以盡管INI值是文本,BeanUtils知道如何將字符串值的原始類型,然后調用相應的JavaBean的setter方法。
引用值
如果需要設置的值不是原始類型而是其它對象呢? 你可以使用$來引用先前定義的實例.例如:
...
sha256Matcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
...
myRealm.credentialsMatcher = $sha256Matcher ...
這會簡單地定位名為sha256Matcher 的對象,然后使用 BeanUtils 在 myRealm 實例上來設置這個對象(通過調用myRealm.setCredentialsMatcher(sha256Matcher)方法).
內嵌屬性
在=的左邊使用點號分隔法來對內嵌屬性進行設值.例如:
...
securityManager.sessionManager.globalSessionTimeout = 1800000 ...
用BeanUtils轉換成下面的邏輯為:
securityManager.getSessionManager().setGlobalSessionTimeout(1800000);
圖的遍歷可以根據需要來設置深度: object.property1.property2....propertyN.value = blah
BeanUtils Property Support
Any property assignment operation supported by the BeanUtils.setProperty method will work in Shiro's [main] section, including set/list/map element assignments. Byte 數組值
由于原始字節數組不能文本格式進行指定,我們必須使用字節數組的文本編碼.這些值可以通過Base64 編碼字符串(默認)或 Hex編碼字符串進行指定.
默認是Base64 ,因為 Base64編碼需要更少的實際文本來表現值- 它有一個更大的編碼字母表,這意味著你的令牌更短(對于文本配置更好).
# The 'cipherKey' attribute is a byte array. By default, text values
# for all byte array properties are expected to be Base64 encoded:
securityManager.rememberMeManager.cipherKey = kPH+bIxk5D2deZiIxcaaaA== ...
然而,如果你選擇使用Hex編碼, 你必須使用0x ('zero' 'x')作為字符串的前輟:
securityManager.rememberMeManager.cipherKey = 0x3707344A4093822299F31D008
Collection屬性
Lists, Sets 和Maps 也可以像其它屬性來設置 -要么直接設置,要么通過內嵌屬性設置. 對于sets和Lists,只需要以逗號分隔來指定值或對象引用的集合:
sessionListener1 = com.company.my.SessionListenerImplementation
...
sessionListener2 = com.company.my.other.SessionListenerImplementation
...
securityManager.sessionManager.sessionListeners = $sessionListener1, $sessionListener2
對于Maps,你可以以逗號分隔的key-value對列表來指定, 每個key-value對都由冒號來分隔
object1 = com.company.some.Class
object2 = com.company.another.Class
...
anObject = some.class.with.a.Map.property
anObject.mapProperty = key1:$object1, key2:$object2
In the above example, the object referenced by $object1 will be in the map under the String key key1, i.e. map.get("key1") returns object1. You can also use other objects as the keys:
anObject.map = $objectKey1:$objectValue1, $objectKey2:$objectValue2 ...
注意事項
順序影響
雖然INI格式非常方便和易于理解,但它并沒有像其它基于text/XML配置機制強大. 使用上述機制時,要理解的最重要的事情是順序帶來的影響.
小心
Each object instantiation and each value assignment is executed in the order they occur in the 在每個[main] 分段中,每個對象實例和及值的分配都是出現順序來執行的.
這些行最終都會轉換成對avaBeans getter/setter 方法的調用,且這些方法也是按相同順序來調用的!在編寫配置時,要當心.
覆蓋實例
任何對象都可以通過配置文件后面新定義的實例來覆蓋.因此對于下面的例子,第二個myRealm定義將覆蓋前面一個:
...
myRealm = com.company.security.MyRealm
...
myRealm = com.company.security.DatabaseRealm ...
譯者注:如果這兩個key在同一個段內,就與先前的描述違背了.
這個導致myRealm成為com.company.security.DatabaseRealm的實例,先前的實例將不再使用(垃圾回收掉).
默認SecurityManager
你可能已經注意到了,上面的例子中沒有定義 SecurityManager 實例, 我們跳到右邊只設置了一個內嵌屬性:
myRealm = ...
securityManager.sessionManager.globalSessionTimeout = 1800000 ...
這是因為securityManager實例是特定的一個- 它已經為你實例化好了,因此你不需要了解具體的SecurityManager實現.
當然,如果你想指定你自己的實現,你可以定義你的實現來進行覆蓋:
... securityManager = com.company.security.shiro.MyCustomSecurityManager ...
當然,極少需要這樣- Shiro的SecurityManager實現是可充分定制的,通??梢园慈魏涡枰M行配置.
[users]
[users] 分段允許你定義一些靜態用戶帳戶集合. 這在一個非常小的用戶帳戶或用戶帳戶不需要在運行時動態創建環境中有用. 下面是例子:
[users]
admin = secret lonestarr = vespa, goodguy, schwartz darkhelmet = ludicrousspeed, badguy, schwartz
Automatic IniRealm
你可以像上面一樣來配置其它任何對象.
行格式
[users]分段中的每行都有下面的格式:
username = password, roleName1, roleName2, ..., roleNameN
- 等號左邊的值是用戶名
- 等號右邊的第一個值是用戶的密碼.密碼是必須的.
- 密碼后面以逗號分隔的值是為用戶分配的角色.角色名稱是可選的.
密碼加密
如果你不想[users]分段中的密碼是明文的,你可以使用你喜歡的hash算法(MD5, Sha1, Sha256)來對其加密,并使用結果字符串來作為密碼值.
默認情況下期望的密碼字符串是Hex編碼的,但你可以使用Base64編碼來代替(見下面).
Easy Secure Passwords
一旦你指定了哈稀化的文本值, 你必須告訴Shiro它們是加密的. 你可為隱式創建的iniRealm 在[main]分段中來配置適當的對應你指定的哈希算法的CredentialsMatcher實現:
[main]
...
sha256Matcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
...
iniRealm.credentialsMatcher = $sha256Matcher
...
[users]
# user1 = sha256-hashed-hex-encoded password, role1, role2,
...
user1 = 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b, role1, role2, ...
你可在CredentialsMatcher上配置任何屬性,如反映哈稀策略的對象,例如, 是否使用鹽(salting)或者執行多少次hash迭代.
例如,如果你想對用戶密碼使用Base64編碼來代替默認的Hex,你可以按如下來指定:
[main]
...
# true = hex, false = base64:
sha256Matcher.storedCredentialsHexEncoded = false
[roles]
再次說明,這對于少量角色或在運行時不需要動態創建角色的環境有用.下面是一個例子:
[roles]
# 'admin' 的角色擁有所有權限, 使用通配符'*'來表示 admin = *
# 'schwartz' 角色可在任何lightsaber上執行任何操作(*): schwartz = lightsaber:*
# 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with # license plate 'eagle5' (instance specific id) goodguy = winnebago:drive:eagle5
行格式
[roles]分段中的每行必須按下面的格式來定義role-to-permission(s) key/value:
rolename = permissionDefinition1, permissionDefinition2, ..., permissionDefinitionN
內部逗號
注意,如果在單個permissionDefinition 中存在逗號分隔符(如. printer:5thFloor:print,info),你必須使用雙引號,以避免解析錯誤:
"printer:5thFloor:print,info"
無權限的角色
如果角色不需要關聯權限,那么則不需要在[roles]分段中列出.如果角色不存在,只需要在[users]分段中定義角色就足夠創建角色了.
[urls]