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