# 14. Custom Subjects 自定義 Subject
# 14. Custom Subjects 自定義 Subject
毫無疑問,在 Apache Shiro 中最重要的概念就是 Subject。 'Subject' 僅僅是一個安全術語,是指應用程序用戶的特定安全的“視圖”。一個 Shiro Subject 實例代表了一個單一應用程序用戶的安全狀態和操作。
這些操作包括:
- authentication(login)
- authorization(access control)
- session access
- logout
我們原本希望把它稱為"User"由于這樣“很有意義”,但是我們決定不這樣做:太多的應用程序現有的 API 已經有自己的 User classes/frameworks,我們不希望和這些起沖突。此外,在安全領域,"Subject" 這一詞實際上是公認的術語。
Shiro 的 API 為應用程序提供 Subject 為中心的編程范式支持。當編碼應用程序邏輯時,大多數應用程序開發人員想知道誰才是當前正在執行的用戶。雖然應用程序通常能夠通過它們自己的機制( UserService 等)來查找任何用戶,但涉及到安全性時,最重要的問題是“誰才是當前的用戶?”。
雖然通過使用 SecurityManager 可以捕獲任何 Subject,但只有基于當前 用戶/Subject 的應用程序代碼更自然,更直觀。
## The Currently Executing Subject 當前執行的Subject
幾乎在所有環境下,你能夠獲得當前執行的 Subject 通過使用
```
org.apache.shiro.SecurityUtils:Subject currentUser
```
getSubject() 方法調用一個獨立的應用程序,該應用程序可以返回一個在應用程序特有位置上基于用戶數據的Subject,在服務器環境中(如,Web 應用程序),它基于與當前線程或傳入的請求相關的用戶數據上獲得 Subject 。
當你獲得了當前的 Subject 后,你能夠拿它做些什么?
如果你想在他們當前的 session 中使事情對用戶變得可用,你可得的他們的 session:
```
Session session = currentUser.getSession();
session.setAttribute( "someKey", "aValue" );
```
Session 是一個 Shiro 的具體實例,它提供了大多數你經常要和HttpSessions 用到的東西,但有一些額外的好處和一個很大的區別:它不需要一個 HTTP 環境!
如果在 Web 應用程序內部部署,默認的 Session 將會是基于HttpSession 的。但是,在一個非 Web 環境中,像這個簡單的 Quickstart,Shiro 將會默認自動地使用它的 Enterprise Session Management。這意味著你可以在你的應用程序中使用相同的 API,在任何層,無論部署環境。這打開了應用程序的全新世界,由于任何需要 session 的應用程序不再被強迫使用 HttpSession 或 EJB Stateful Session Beans。而且,任何客戶端技術現在能夠共享會話數據。
所以,你現在可以獲取一個 Subject 以及他們的 Session。對于真正有用的東西像檢查會怎么樣呢,如果他們被允許做某些事——如對角色和權限的檢查?
嗯,我只能對已知的用戶做這些檢查。我們的 Subject 實例代表了當前的用戶,但誰又是實際上的當前用戶呢?呃,他們都是匿名的——也就是說,直到他們至少登錄一次。那么,讓我們像下面這樣做:
```
if ( !currentUser.isAuthenticated() ) {
//collect user principals and credentials in a gui specific manner
//such as username/password html form, X509 certificate, OpenID, etc.
//We'll use the username/password example here since it is the most common.
//(do you know what movie this is from? ;)
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
//this is all you have to do to support 'remember me' (no config - built in!):
token.setRememberMe(true);
currentUser.login(token);
}
```
那就是了!它再簡單不過了。
但如果他們的登錄嘗試失敗了會怎么樣?你可以捕獲各種各樣的具體的異常來告訴你到底發生了什么:
```
try {
currentUser.login( token );
//if no exception, that's it, we're done!
} catch ( UnknownAccountException uae ) {
//username wasn't in the system, show them an error message?
} catch ( IncorrectCredentialsException ice ) {
//password didn't match, try again?
} catch ( LockedAccountException lae ) {
//account for that username is locked - can't login. Show them a message?
}
... more types exceptions to check if you want ...
} catch ( AuthenticationException ae ) {
//unexpected condition - error?
}
```
你,作為 應用程序/GUI 開發人員,可以基于異常選擇是否顯示消息給終端用戶(例如,“在系統中沒有與該用戶名對應的帳戶。”)。有許多不同種類的異常供你檢查,或者你可以拋出你自己自定義的異常,這些異常可能是Shiro 還未提供的。有關詳情,請查看AuthenticationException 的[JavaDoc](http://www.jsecurity.org/api/org/jsecurity/authc/AuthenticationException.html)。
好了,現在,我們有了一個登錄的用戶,我們還有什么可以做的呢?
比方說,他們是誰:
```
//print their identifying principal (in this case, a username):
log.info( "User [" + currentUser.getPrincipal() + "] logged in successfully." );
```
我們還可以測試他們是否有特定的角色:
```
if ( currentUser.hasRole( "schwartz" ) ) {
log.info("May the Schwartz be with you!" );
} else {
log.info( "Hello, mere mortal." );
}
```
我們還能夠判斷他們是否有[權限](http://shiro.apache.org/permissions.html)對一個確定類型的實體進行操作
```
if ( currentUser.isPermitted( "lightsaber:weild" ) ) {
log.info("You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only.");
}
```
此外,我們可以執行一個非常強大的實例級權限檢查——它能夠判斷用戶是否能夠訪問一個類型的具體實例:
```
if ( currentUser.isPermitted( "winnebago:drive:eagle5" ) ) {
log.info("You are permitted to 'drive' the 'winnebago' with license plate (id) 'eagle5'. " +
"Here are the keys - have fun!");
} else {
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}
```
小菜一碟,對吧? 最后,當用戶完成了對應用程序的使用時,他們可以注銷:
```
currentUser.logout(); //removes all identifying information and invalidates their session too.
```
這個簡單的API 包含了 90% 的 Shiro 終端用戶在使用 Shiro 時將會處理的東西。
## Custom Subject Instances 自定義 Subject 實例
Shiro 1.0 中添加了一個新特性,能夠在特殊情況下構造 自定義/臨時 的subject 實例。
*只在特殊情況下使用!*
*你應該總是通過調用 SubjectUtils.getSubject() 來獲得當前正在執行的 Subject; 創建自定義的 Subject 實例只應在特殊情況下進行。*
當一些“特殊情況”是,這是可以很有用的:
- 系統啟動/引導——當沒有用戶月系統交互時,代碼應該作為一'system'或daemon 用戶來執行。創建 Subject實例來代表一個特定的用戶是值得的,這樣引導代碼能夠以該用戶(如admin)來執行。 鼓勵這種做法是由于它能保證 utility/system 代碼作為一個普通用戶以同樣的方式執行,以確保代碼是一致的。 這使得代碼更易于維護,因為你不必擔心 system/daemon 方案的自定義代碼塊。
- 集成測試——你可能想創建 Subject 實例,在必要時可以在集成測試中使用。請參閱[測試](13.%20Testing%20%E6%B5%8B%E8%AF%95.md)文檔獲取更多的內容。
- Daemon/background 進程的工作——當一個 daemon 或 background 進程執行時,它可能需要作為一個特定的用戶來執行。
*如果你已經有一個 Subject 的實例,并希望它提供給其他線程,你應該使用 `Subject.associateWith*` 方法,而不是創建一個新的Subject 實例。\*
好了,假設你仍然需要創建自定義的Subject 實例的情況下,讓我們看看如何來做:
### Subject.Builder
Subject.Builder 被制定得非常容易創建 Subject 實例,而無需知道構造細節。 Builder 最簡單的用法是構造一個匿名的,session-less(無會話) Subject 的實例。
```
Subject subject = new Subject.Builder().buildSubject()
```
上面所展示的默認的Subject.Builder 無參構造函數將通過SecurityUtils.getSubject() 方法使用應用程序當前可訪問的 SecurityManager 。你也可以指定被額外的構造函數使用的SecurityManager 實例,如果你需要的話:
```
SecurityManager securityManager = //acquired from somewhere
Subject subject = new Subject.Builder(securityManager).buildSubject();
```
所有其他的 Subject.Builder 方法可以在 buildSubject()方法之前被調用,它們來提供關于如何構造 Subject 實例的上下文。例如,假如你擁有一個 session ID ,想取得“擁有”該 session 的Subject(假設該 session 存在且未過期):
```
Serializable sessionId = //acquired from somewhere
Subject subject = new Subject.Builder().sessionId(sessionId).buildSubject();
```
同樣地,如你想創建一個Subject 實例來反映一個確定的身份:
```
Object userIdentity = //a long ID or String username, or whatever the "myRealm" requires
String realmName = "myRealm";
PrincipalCollection principals = new SimplePrincipalCollection(userIdentity, realmName);
Subject subject = new Subject.Builder().principals(principals).buildSubject();
```
然后,你可以使用構造的 Subject 實例,如預期一樣對它進行調用。但請注意:
構造的 Subject 實例不會由于應用程序(線程)的進一步使用而自動地綁定到應用程序(線程)。如果你想讓它對于任何代碼都能夠方便地調用SecurityUtils.getSubject(),你必須確保創建好的 Subject 有一個線程與之關聯。
### Thread Association 線程關聯
如上所述,只是構建一個 Subject 實例,并不與一個線程相關聯——一個普通的必要條件是在線程執行期間任何對 SecurityUtils.getSubject() 的調用是否能正常工作。確保一個線程與一個 Subject 關聯有三種途徑:
- Automatic Association(自動關聯)—— 通過 Sujbect.execute\* 方法執行一個Callable 或Runnable 方法會自動地綁定和解除綁定到線程的Subject,在Callable/Runnable 異常的前后。
- Manual Association(手動關聯)——你可以在當前執行的線程中手動地對Subject 實例進行綁定和解除綁定。這通常對框架開發人員非常有用。
- Different Thread(不同的線程)——通過調用Subject.associateWith\* 方法將 Callable 或 Runnable 方法關聯到 Subject,然后返回的 Callable/Runnable 方法在另一個線程中被執行。如果你需要為Subject 在另一個線程上執行工作的話,這是首選的方法。
了解線程關聯最重要的是,兩件事情必須始終發生:
1. Subject 綁定到線程,所以它在線程的所有執行點都是可用的。Shiro 做到這點通過它的 ThreadState 機制,該機制是在 ThreadLocal 上的一個抽象。
2. Subject 將在某點解除綁定,即使線程的執行結果是錯誤的。這將確保線程保持干凈,并在pooled/reusable 線程環境中清除任何之前的Subject 狀態。
這些原則保證在上述三個機制中發生。接下來闡述它們的用法。
#### Automatic Association 自動關聯
如果你只需要一個 Subject 暫時與當前的線程相關聯,同時你希望線程綁定和清理自動發生,Subject 的 Callable 或 Runnable 的直接執行正是你所需要的。在 Subject.execute 調用返回后,當前線程被保證當前狀態與執行前的狀態是一樣的。這個機制是這三個中使用最廣泛的。
例如,讓我們假定你有一些邏輯在系統啟動時需要執行。你希望作為一個特定用戶執行代碼塊,但一旦邏輯完成后,你想確保 線程/環境 自動地恢復到正常。你可以通過調用 Subject.execute\* 方法來做到:
```
Subject subject = //build or acquire subject
subject.execute( new Runnable() {
public void run() {
//subject is 'bound' to the current thread now
//any SecurityUtils.getSubject() calls in any
//code called from here will work
}
});
//At this point, the Subject is no longer associated
//with the current thread and everything is as it was before
```
當然,Callable 的實例也能夠被支持,所以你能夠擁有返回值并捕獲異常:
```
Subject subject = //build or acquire subject
MyResult result = subject.execute( new Callable<MyResult>() {
public MyResult call() throws Exception {
//subject is 'bound' to the current thread now
//any SecurityUtils.getSubject() calls in any
//code called from here will work
...
//finish logic as this Subject
...
return myResult;
}
});
//At this point, the Subject is no longer associated
//with the current thread and everything is as it was before
```
這種方法在框架開發中也是很有用的。例如,Shiro 對 secure Spring remoting 的支持確保了遠程調用能夠作為一個特 定的 Subject 來執行:
```
Subject.Builder builder = new Subject.Builder();
//populate the builder's attributes based on the incoming RemoteInvocation
...
Subject subject = builder.buildSubject();
return subject.execute(new Callable() {
public Object call() throws Exception {
return invoke(invocation, targetObject);
}
});
```
#### Manual Association 手動關聯
雖然 Subject.execute\* 方法能夠在它們返回后自動地清理線程的狀態,但有可能在一些情況下,你想自己管理 ThreadState。當結合 w/Shiro 時,這幾乎總是在框架開發層次使用,但它很少在 bootstrap/daemon 情景下使用(上面 Subject.execute(callable) 例子使用得更為頻繁)。
*Guarantee Cleanup*
*關于這一機制最重要的是,你必須一直保證當前的線程在邏輯執行完后被清理,以確保在一個可重復使用或線程池的環境中沒有一個線程狀態腐化。*
最好的做法是在try/finally 塊保證清理:
```
Subject subject = new Subject.Builder()...
ThreadState threadState = new SubjectThreadState(subject);
threadState.bind();
try {
//execute work as the built Subject
} finally {
//ensure any state is cleaned so the thread won't be
//corrupt in a reusable or pooled thread environment
threadState.clear();
}
```
有趣的是,這正是 Subject.execute\* 方法實際上所做的——它們只是在Callable 或 Runnable 執行前后自動地執行這個邏輯。Shiro 的 ShiroFilter 為 Web 應用程序執行幾乎相同的邏輯(ShiroFilter 使用 Web 特定的 ThreadState 的實現,超出了本節的范圍)
*Web Use*
*不要在一個處理 Web 請求的進程中使用上述 ThreadState 代碼示例。 Web 特定的 ThreadState 的實現使用 Web 請求代替。相反,確保ShiroFilter 攔截 Web 請求以確保 Subject 的 building/binding/cleanup 能夠好好的完成。*
#### A Different Thread
如果你有一個 Callable 或 Runnable 實例要以 Subject 來執行,你將自己執行 Callable 或 Runnable(或這將它移交給線程池或執行者或ExcutorService),你應該使用 Subject.associateWith\* 方法。這些方法確保在最終執行的線程中保 留 Subject,且該 Subject 是可訪問的。
Callable 例子:
```
Subject subject = new Subject.Builder()...
Callable work = //build/acquire a Callable instance.
//associate the work with the built subject so SecurityUtils.getSubject() calls works properly:
work = subject.associateWith(work);
ExecutorService executorService = new java.util.concurrent.Executors.newCachedThreadPool();
//execute the work on a different thread as the built Subject:
executor.execute(work);
```
Runnable 例子:
```
Subject subject = new Subject.Builder()...
Runnable work = //build/acquire a Runnable instance.
//associate the work with the built subject so SecurityUtils.getSubject() calls works properly:
work = subject.associateWith(work);
Executor executor = new java.util.concurrent.Executors.newCachedThreadPool();
//execute the work on a different thread as the built Subject:
executor.execute(work);
```
*Automatic Cleanup*
*associateWith 方法自動執行必要的線程清理,以取保現在在線程池環境中的clean。*
## 為文檔加把手
我們希望這篇文檔可以幫助你使用 Apache Shiro 進行工作,社區一直在不斷地完善和擴展文檔,如果你希望幫助 Shiro 項目,請在你認為需要的地方考慮更正、擴展或添加文檔,你提供的任何點滴幫助都將擴充社區并且提升 Shiro。
提供你的文檔的最簡單的途徑是將它發送到用戶[論壇](http://shiro-user.582556.n2.nabble.com/)或[郵件列表](http://shiro.apache.org/mailing-lists.html)
*譯者注:*如果對本中文翻譯有疑議的或發現勘誤歡迎指正,[點此](https://github.com/waylau/apache-shiro-1.2.x-reference/issues)提問。
- Introduction
- 1. Introduction 介紹
- 2. Tutorial 教程
- 3. Architecture 架構
- 4. Configuration 配置
- 5. Authentication 認證
- 6. Authorization 授權
- 6.1. Permissions 權限
- 7. Realms
- 8. Session Management
- 9. Cryptography 密碼
- 10. Web
- 10.1. Configuration 配置
- 10.2. 基于路徑的 url 安全
- 10.3. Default Filters 默認過濾器
- 10.4. Session Management
- 10.5. JSP Tag Library
- 11. Caching 緩存
- 12. Concurrency & Multithreading 并發與多線程
- 13. Testing 測試
- 14. Custom Subjects 自定義 Subject
- 15. Spring Framework
- 16. Guice
- 17. CAS
- 18. Command Line Hasher
- 19. Terminology 術語
- 20. 10 Minute Tutorial 十分鐘教程
- 21. Beginner's Webapp Tutorial 初學者web應用教程
- 22. Application Security With Apache Shiro 用Shiro保護你的應用安全
- 23. CacheManager 緩存管理
- 24. Apache Shiro Cryptography Features 加密功能