本文配套源碼
在Java 2之前的版本,運行時的安全模型使用非常嚴格受限的沙箱模型(Sandbox)。讀者應該熟悉,Java 不受信的Applet代碼就是基於這個嚴格受限的沙箱模型來提供運行時的安全檢查。沙箱模型的本質是,任何本地運行的代碼都是受信的,有完全的權限來存取關鍵的系統資源。而對於Applet,則屬於不受信的代碼,只能訪問沙箱范圍內有限的資源。當然,您可以通過數字簽名的方式配置您的Applet為受信的代碼,具有同本地代碼一樣的權限。
從Java 2開始,Java 提供了基於策略(Policy)與堆棧授權的運行時安全模型,它是一個更加細粒度的存取控制,易於配置與擴展,其總體的架構如圖 1 所示:
圖 1.Java 2安全模型
簡單來講,當類由類裝載器(Class Loader)載入到 JVM 運行,這些運行時的類會根據 Java Policy 文件的配置,被賦予不同的權限。當這些類要訪問某些系統資源(例如打開 Socket、讀寫文件等),或者執行某些敏感的操作(例如存取密碼)時,Java 的安全管理器(ava.lang.SecuirtyManager)的檢查權限方法將被調用,檢查這些類是否具有必要的權限來執行該操作。
在繼續深入討論之前,我們先來澄清下面的幾個概念:
策略,即系統安全策略,由用戶或者管理員配置,用來配置執行代碼的權限。運行時的 java.security.Policy 對象用來代表該策略文件。
權限,Java 定義了層次結構的權限對象,所有權限對象的根類是 java.security.Permission。權限的定義涉及兩個核心屬性:目標(Target)與動作 (Action)。例如對於文件相關的權限定義,其目標就是文件或者目錄,其動作包括:讀,寫,刪除等。
保護域,保護域可以理解為具有共同的權限集的類的集合。
在Java 2裡,權限實際上是被賦予保護域的,而不是直接賦給類。權限、保護域和類之間的映射關系如圖 2。
圖 2. 類,保護域,權限的映射關系
如圖 2 所示,當前運行時堆棧是從 a.class 到 e.class。在運行時堆棧上的每一幀(Stack Frame)都會被 Java 劃歸為某個保護域(保護域是 Java 根據 Policy 文件配置構建出來的)。Java 的安全管理器在執行權限檢查時,會對堆棧上的每個 Stack Frame 做權限檢查,當且僅當每個 Stack Frame 被賦予的權限集都暗含(Imply)了所要求的權限時,該操作才被允許執行,否則 java.security.AccessControlException 異常將被拋出,該操作執行失敗。
有關Java 2安全模型,有幾點需要特別說明:
該模型是基於堆棧授權的,這在多線程的環境下,同樣適用。例如當父線程創建了子線程,子線程的執行被看作是父線程執行的繼續,所以 Java 的安全管理器在權限檢查時,所檢查的運行時堆棧,既包括當前子線程的,也包括從父線程那裡繼承過來的運行時堆棧。這意味著,用戶不可能通過線程的創建來獲得額外的權限。
Java 的開發者可以使用 AccessController.doPrivileged 來優化權限檢查帶來的額外性能開銷。如圖 3 所示,Java 的權限檢查將從堆棧的頂部開始,逐一向下,直到碰到 doPrivileged 的方法調用,或者到達堆棧底部為止。使用 doPrivileged 可以避免不必要的棧遍歷(Stack Traverse),提高程序的性能。
在該模型中,有一個特殊的保護域,系統域(System Domain)。所有被 null類裝載器所裝載的類都被稱為系統代碼,其自動擁有所有的權限。而且,所有的重要的受保護的外部資源,如文件系統、網絡、屏幕、鍵盤等只能通過系統代碼獲得。
圖 3. doPrivileged Stack Frame
接下來,本文會給出一個簡單的示例,然後我們根據這個示例,進一步深入,來創建一個線程間安全協作的應用。
示例
我們的示例很簡單:客戶端調用 LogService 提供的 API,把 Message 寫入到磁盤文件。
清單 1. 客戶端程序
package sample.permtest.client;
……
public class Client {
……
public static void main(String[] args) {
//構造消息日志,使用LogService將其寫入c:\\paper\\client\\out.tmp文件。
Message message = new Message("c:\\paper\\client\\out.tmp",
"Hi, this is called from client"+'\n');
LogService.instance.log(message);
//構造消息日志,使用LogService將其寫入c:\\paper\\server\\out.tmp文件。
message = new Message("c:\\paper\\server\\out.tmp",
"Hi, this is called from client"+'\n');
LogService.instance.log(message);
}
}
清單 2. LogService
package sample.permtest.server;
……
public class LogService {
……
public void log(Message message) {
final String destination = message.getDestination();
final String info = message.getInfo();
FileWriter filewriter = null;
try
{
filewriter = new FileWriter(destination, true);
filewriter.write(info);
filewriter.close();
}
catch (IOException ioexception)
{
ioexception.printStackTrace();
}
}
}
如清單 1、2 所示,這就是一個普通的 Java 應用程序。我們把這個程序放在 Java 的安全模型中執行。Client 類放在 client.jar JAR 包裡,而 LogService 類放在 server.jar JAR 包裡
首先我們使用 keytool 工具來生成我們需要的 keystore 文件,以及需要的數字證書,如清單 3 所示。
清單 3. 生成 keystore 文件及其數字證書
>keytool -genkey -alias client -keyalg RSA -keystore C:\paper\.keystore
>keytool -genkey -alias server -keyalg RSA -keystore C:\paper\.keystore
在清單 3 中,我們生成了 C:\paper\.keystore 文件,使用 RSA 算法生成了別名為 client 與 server 的兩個數字證書。(注 : 為方便起見,keystore 與 client,server 證書的密鑰都是 111111)
我們使用如清單 4 所示的命令來簽名 client.jar 與 server.jar。
清單 4. 簽名 JAR 文件
>jarsigner.exe -keystore C:\paper\.keystore
-storepass 111111 c:\paper\client.jar client
>jarsigner.exe -keystore C:\paper\.keystore
-storepass 111111 c:\paper\server.jar server
在清單 4 中,我們使用了別名為 client 的數字證書來簽名 client.jar 文件,使用別名為 server 的數字證書來簽名 server.jar 文件。
使用圖形化的工具 policytool.exe 創建清單 5 所示的 Policy 文件。
清單 5. Policy 文件
/* AUTOMATICALLY GENERATED ON Thu May 14 15:40:25 CST 2009*/
/* DO NOT EDIT */
keystore "file:////C:/paper/.keystore";
grant signedBy "client" {
permission java.io.FilePermission "c:\\paper\\client\\*","read,write";
};
grant signedBy "server" {
permission java.security.AllPermission;
};
Policy 文件指出,所有被”client”簽名的代碼具有讀寫” c:\\paper\\client\\”目錄下所有文件的權限,而所有被”server”簽名的代碼具有所有的權限。Java 將根據該策略文件按照簽名者創建相應的保護域。
一切就緒,我們運行代碼,如清單 6 所示。
清單 6. 運行程序
>java -Djava.security.manager
-Djava.security.policy=my.policy -classpath client.jar;server.jar
sample.permtest.client.Client
有兩個運行時選項特別重要,-Djava.security.manager 告訴 JVM 裝載 Java 的安全管理器,進行運行時的安全檢查,而 -Djava.security.policy 用來指定我們使用的策略文件。
運行的結果如清單 7 所示。
清單 7. 運行結果
Exception in thread "main" java.security.AccessControlException: access denied (
java.io.FilePermission c:\paper\server\out.tmp write)
at java.security.AccessControlContext.checkPermission(Unknown Source)
at java.security.AccessController.checkPermission(Unknown Source)
at java.lang.SecurityManager.checkPermission(Unknown Source)
at java.lang.SecurityManager.checkWrite(Unknown Source)
at java.io.FileOutputStream.<init>(Unknown Source)
at java.io.FileOutputStream.<init>(Unknown Source)
at java.io.FileWriter.<init>(Unknown Source)
at sample.permtest.server.LogService.log(LogService.java:19)
at sample.permtest.client.Client.main(Client.java:16)
客戶端運行後,第一條消息成功寫入 c:\\paper\\client\\out.tmp 文件,而第二條消息由於沒有 c:\paper\server\out.tmp 文件的寫權限而被拒絕執行。
線程間的安全協作
前一節本文給出的示例,如果放在線程間異步協作的環境裡,情況會變得復雜。如圖 4 所示。
圖 4. 線程的異步協作
如圖 4,在這樣的情景下,客戶端線程的運行時堆棧完全獨立於服務器端的線程,它們之間僅僅通過共享的數據結構消息隊列進行異步協作。例如:當客戶端線程放入 Message X,而後,服務器端的線程拿到 Message X 進行處理,我們仍然假設 Message X 是希望服務器端線程將消息寫入 c:\paper\server\out.tmp 文件。在這個時候,服務程序怎樣才能確保客戶端具有寫入 c:\paper\server\out.tmp 文件的權限?
Java 提供了基於線程協作場景的解決方案,如清單 8 所示:
清單 8. 線程協作版本的 LogService
package sample.permtest.thread.server;
……
public class LogService implements Runnable
{
……
public synchronized void log(Message message)
{
//該方法將在客戶端線程環境中執行
//在消息放入隊列的時候,我們把客戶端線程的執行環境通過
//AccessController.getContext() 得到,
//並及時保存下來。
message.m_accesscontrolcontext = AccessController.getContext();
_messageList.add(message);
notifyAll();
}
……
//從隊列中取出消息,並逐一處理
public void run()
{
while (true)
{
Message message = null;
try
{
message = retrieveMessage();
}
catch (InterruptedException interruptedexception)
{
break;
}
final String destination = message.getDestination();
final String stringMessage = message.getInfo();
AccessController.doPrivileged
(
new PrivilegedAction()
{
public Object run()
{
FileWriter filewriter = null;
try
{
filewriter = new FileWriter(destination, true);
filewriter.write(stringMessage);
filewriter.close();
}
catch (IOException ioexception)
{
ioexception.printStackTrace();
}
return null;
}
},
message.m_accesscontrolcontext
//將客戶端線程當時的執行環境傳入,進行權限檢查。
);
}
}
}
消息類的 m_accesscontrolcontext 成員變量是一個 AccessControlContext 對象,它封裝的當前線程的執行環境快照,我們可以通過調用 AccessController 的 getContext 方法獲得。安全的線程協作工作原理如圖 5 所示。
圖 5. 線程異步協作權限檢查路徑
圖 5 中的箭頭指示了 Java 的安全管理器權限檢查的路徑,從當前的幀 (Frame) 開始,沿著服務器端線程的運行時堆棧檢查,直到碰到了 AccessController.doPrivileged 幀。由於我們在調用 doPrivileged 方法時,傳入了 m_accesscontrolcontext,也就是客戶端線線程在往消息隊列裡插入消息時的執行環境,所以 Java 的安全管理器會跳轉到該執行環境,沿著客戶端插入消息時的執行堆棧逐一檢查。
在本節線程版本的 Log 服務實現中,Client 類在 sample.permtest.thread.client 包裡,該包被導出為 thread_client.jar JAR 包,而 LogService 在 sample.permtest.thread.server 包裡,該包被導出為 thread_server.jar JAR 包。而有關這部分的包簽名與上節類似,使用了與上節相同的數字證書。
關於完整的源代碼,讀者可以在本文後面的資源列表中下載。
小結
本文通過示例,詳盡描述了Java 2運行時的安全模型特性,以及基於該模型,如何構建安全的線程協作應用。值得一提的是,當您的 Java 應用使用的Java 2所提供的運行時安全模型,程序性能的降低是必然的,因為我們已經看到,Java 2 的安全模型是基於堆棧授權的,這意味著,每一次 Java 安全管理器檢查權限方法的執行,都會遍歷當前運行時行堆棧的所有幀,以確定是否滿足權限要求。所以您的設計一定要在安全與性能之間取捨。當然,當您在應用了 Java 的安全模型後,您仍然有機會進行性能的優化,比如使用 doPrivileged 方法去掉不必要的堆棧遍歷,更進一步,您可以根據自己應用的特點,通過繼承 java.lang. SecurityManager 類,來開發適合自己應用的安全管理器。