Java安全模型側重于保護終端用戶免受從網絡下載的、來自不可靠來源的、惡意程序(以及善意程序中的bug)的侵犯。為了達到這個目的,Java提供了一個用戶可配置的“沙箱”,在沙箱中可以放置不可靠的Java程序。
2.1 基本沙箱
沙箱模型使你可以接收來自任何來源的代碼,而不是要求用戶避免將來自不受信站點的代碼下載到機器上。
組成Java沙箱的基本組件如下:
- 類裝載器結構
- class文件檢驗器
- 內置于Java虛擬機(及語言)的安全特性
- 安全管理器及Java API
2.2 類裝載器體系結構
在Java沙箱中,類裝載器是第一道防線:
- 它防止惡意代碼去干涉善意的代碼
- 它守護了被信任的類庫的邊界
- 它將代碼歸入某類(稱為保護域),該類確定了代碼可以進行哪些操作
類裝載器體系結構可以防止惡意的代碼去干涉善意的代碼,是通過為由不同的類裝載器裝入的類提供不同的命名空間來實現的。命名空間由一系列唯一的名稱組成,每一個被裝載的類有一個名字,這個命名空間是由Java虛擬機為每一個類裝載器維護的。
類裝載器體系結構守護了被信任的類庫的邊界,是通過分別使用不同的類裝載器裝載可靠的包和不可靠的包來實現的。
從Java 1.2版本開始,類裝載器請求另一個用戶自定義類裝載器來裝載一個類型的過程被形式化,稱為雙親委派模式。除啟動類裝載器外的每一個類裝載器,都有一個“雙親”類裝載器,在某個特定的類裝載器試圖以常用方式裝載類型之前,它會先默認地將這個任務“委派”給它的雙親——請求它的雙親來裝載這個類型。
運行時包,是指由同一個類裝載器裝載的、屬于同一個包的、多個類型的集合。在允許兩個類型之間對包內可見的成員進行訪問前,虛擬機不但要確定這兩個類型屬于同一個包,還必須確認它們屬于同一個運行時包——它們必須是由同一個類裝載器裝載的。
除了屏蔽不同命名空間的類以及保護受信任的類庫的邊界外,類裝載器還起到了另外的安全作用。類裝載器必須將每一個被裝載的類放置在一個保護域中,一個保護域定義了這個代碼在運行時將得到怎樣的權限。
2.3 class文件檢驗器
和類裝載器一起,class文件檢驗器保證裝載的class文件內容有正確的內部結構,并且這些class文件相互間協調一致。如果class文件檢驗器在class文件中發現了問題,它會拋出異常。
JVM的class文件檢驗器只在字節碼執行之前進行一次分析,需要進行四趟獨立的掃描:
- 第一趟是在類被裝載時進行的,主要檢查class文件的內部結構
- 第二趟和第三趟是在連接過程中進行的,主要確認類型數據遵從Java編程語言的語義,包括檢驗它所包含的所有字節碼的完整性。
- 第四趟是在動態連接的過程中解析符號時進行的,主要確認被引用的類、字段以及方法確實存在。
2.4 JVM中內置的安全特性
JVM在執行字節碼時還進行其他一些內置的安全機制的操作,包括:
- 類型安全的引用轉換
- 結構化的內存訪問(無指針算法)
- 自動垃圾收集(不必顯式地釋放被分配的內存)
- 數組邊界檢查
- 空引用檢查
2.5 安全管理器和Java API
Java安全模型的前三個組成部分——類裝載器體系結構、class文件檢驗器以及Java中內置的安全特性——一起達到一個共同的目的:保持JVM的實例和它正在運行的應用程序的內部完整性,使得它們不被下載的惡意或有漏洞的代碼侵犯。相反,這個安全模型的第四個組成部分是安全管理器,它主要用于保護虛擬機的外部資源不被虛擬機內運行的惡意或有漏洞的代碼侵犯。
安全管理器定義了沙箱的外部邊界。因為它是可定制的,所以它允許為程序建立自定義的安全策略。
Java應用程序在啟動時不會默認安裝安全管理器,也不會有任何安全限制。在Java 1.2版本以前,如果安裝了安全管理器,那么它將負責應用程序整個剩余的生命周期,不能被替代、擴展或者修改。在1.2版本中,當前安裝的安全管理器能夠在允許的情況下被替換。
安全管理器主要負責兩個方面的工作:說明一個安全策略以及執行這個安全策略。
Java 1.2版本中的java.lang.SecurityManager
是一個具體的類,它提供了一個默認的安全管理器的實現,能夠輔助建立一個基于代碼簽名的細粒度的安全策略。用戶可以顯示實例化這個安全管理器,或者讓它自動安裝。例如,在Sun的Java 2 SDK版本1.2中,可以在命令行使用-Djava.security.manager
選項來指明安裝具體安全管理器。
具體安全管理器類允許用戶不用Java代碼定義自己的安全策略,而是用一個稱為策略文件的ASCII文件。在策略文件中,可以給代碼來源授予權限。權限是用類定義的,它是java.security.Permission
的子類。代碼來源是由代碼庫的URL和一些簽名組成的,從這個URL可以裝載代碼,而簽名則為這個代碼作擔保。當創建安全管理器時,它對策略文件進行解析,并創建CodeSource(代碼來源)和Permission(權限)對象,這些對象被封裝在一個單獨的Policy對象中,這個Policy對象就代表了運行時的策略。任何時刻只能有一個Policy對象被安裝。
類裝載器將類型放到保護域(ProtectionDomain)中,保護域封裝了授予代碼來源的所有權限,這些代碼來源由裝載的類型代表。
當具體安全管理器的check方法被調用時,它們中的大多數都將請求傳遞給一個稱為AcessController的類。這個AccessController使用了包含在保護域對象中的信息,這個對象所屬的類的方法在調用棧中,AccessController進行棧檢查以確定這個操作能否被執行。
在 java.lang.SecurityManager
中有兩個關鍵方法:
- checkPermission(Permission perm) —— 進行某個操作(它需要指定的權限)前被調用
- checkPermission(Permission perm, Object context) —— 在被傳遞的安全上下文中進行某個操作(它需要指定的權限)前被調用
在具體安全管理器類中,checkPermission( )方法同樣負責決定,是否允許將某個操作的任務委派給另一個方法。這個checkPermission( )方法只是簡單地調用了類java.security.AccessController
中的靜態checkPermission( )方法,并將這個Permission對象傳遞給它。
2.6 代碼簽名和認證
Java安全模型很重要的一點就是它支持認證。認證可以使用戶確認,由某些團體擔保的一些class文件是值得信任的,并且這些class文件在到達用戶虛擬機的途中沒有被改變。這樣,如果用戶在一定程度上信任這個為代碼作擔保的團體,也就可以在一定程度上簡化沙箱對這段代碼的限制??梢詫τ刹煌瑘F體簽名的代碼建立不同的安全限制。
2.7 策略
Java安全體系結構的真正好處在于,它可以對代碼授予不同層次的信任度來部分地訪問系統。
版本1.2的安全體系結構的主要目標之一就是使建立(以代碼簽名為基礎的)細粒度的訪問控制策略的過程更為簡單且更少出錯。在版本1.2的安全模型中,權限(系統訪問權限)使授予代碼來源的。
對應于整個Java應用程序的一個訪問控制策略是由抽象類java.security.Policy的一個子類的單個實例所表示的。在任何時候,每個應用程序實際上都只有一個Policy對象。類裝在其利用這個Policy對象來幫助它們決定,在把一段代碼導入虛擬機時應該給它們怎么樣的權限。
安全策略是一個從描述運行代碼的屬性集合到這段代碼所擁有的權限的映射。在版本1.2的安全體系結構中,描述運行代碼的屬性被總稱為代碼來源。
在版本1.2中,所有和具體安全管理器有關的工具和訪問控制體系結構都只對證書起作用,而不能對“赤裸”的公鑰起作用。
權限是由抽象類java.security.Permission
的一個子類的實例表示的。一個Permission對象有三個屬性:類型、名字和可選的操作。
在Policy對象中,每一個CodeSource是和一個或多個Permission對象相關聯的。
策略文件
由Sun提供的在Java 1.2平臺下的具體Policy子類采用如下方法:在一個ASCII策略文件中用上下文無關文法描述安全策略。
keystore "ijvmkeys";
grant signedBy "friend" {
permission java.io.FilePermission "question.txt", "read";
permission java.io.FilePermission "answer.txt", "read";
};
grant signedBy "stranger" {
permission java.io.FilePermission "question.txt", "read";
};
grant codeBase "file:${com.artima.ijvm.cdrom.home}/security/ex2/*" {
permission java.io.FilePermission "question.txt", "read";
permission java.io.FilePermission "answer.txt", "read";
};
最后一條grant語句中的代碼庫URL采用了文件的形式:它包含了一個屬性${com.artima.ijvm.cdrom.home}
。
2.8 保護域
當類裝載器將類型裝入JVM時,它們將為每個類型指派一個保護域。保護域定義了授予一段特定代碼的所有權限。(一個保護域對應于策略文件中的一個或多個grant子句。)裝載入JVM的每一個類型都屬于且僅屬于一個保護域。
雖然前面的Policy對象代表了一個從代碼來源到權限的全局映射,但是最終還是由類裝載器負責決定代碼執行時將獲得怎樣的權限。
圖2-4用圖形化的方式描述了保護域、代碼來源以及權限。ProtectionDomain對象封裝了一個到CodeSource對象的引用以及一個到java.security.Permissions
對象的引用。java.security.Permissions
是抽象類java.security.PermissionCollection
的一個具體類,代表了一個同構權限的集合。
當一個類裝載器將Friend和Friend$1
導入方法區時,類裝載器將把一個ProtectionDomain對象的引用和這些class文件的字節傳遞給defineClass( )方法。這個defineClass( )方法將Friend和Friend$1
所在的方法區中的類型數據和被傳遞的ProtectionDomain對象相關聯。
2.9 訪問控制器
類java.security.AccessController
提供了一個默認的安全策略執行機制,它使用棧檢查來決定潛在不安全的操作是否被允許。
由AccessController的checkPermission( )實現的基本算法決定了調用棧中的每個幀是否有權執行潛在不安全的操作。每一個棧幀代表了由當前線程調用的某個方法,每一個方法是在某個類中定義的,每一個類又屬于某個保護域,每個保護域包含一些權限。因此,每個棧幀間接地和一些權限相關。
當調用doPrivileged( )方法時,就像調用其它任何方法一樣,都會將一個新的棧幀壓入棧。在由AccessController執行的棧檢查中,一個doPrivileged( )方法調用的棧幀標識了檢查過程的提前終止點。如果和調用doPrivileged( )的方法相關聯的保護域擁有執行被請求操作的權限,AccessController將立即返回。這樣這個操作就被允許,即使在棧下層的代碼可能沒有執行這個操作的權限。