Java體系結(jié)構(gòu)采用了一個擴展的內(nèi)置安全模型
Java的安全模型是其多個重要結(jié)構(gòu)特點之一,它使Java成為適用于網(wǎng)絡(luò)環(huán)境的技術(shù)。
Java安全模型側(cè)重于保護終端用戶免受從網(wǎng)絡(luò)下載的、來自不可靠來源的、惡意程序(以及善意程序中的bug)的侵犯。
為了達(dá)到這個目的,Java提供了一個用戶可配置的“沙箱”,在沙箱中可以放置不可靠的Java程序。
沙箱對不可靠程序的活動進行了限制,程序可以在沙箱的安全邊界內(nèi)做任何事,但是不能進行任何跨越這些邊界的舉動。例如:
1.對本地硬盤的讀寫操作
2.進行任何網(wǎng)絡(luò)連接,但不能連接到提供這個applet的源主機,
3.創(chuàng)建新的進程
4.裝載新的動態(tài)連接庫
簽名和認(rèn)證使得接收端系統(tǒng)可以確認(rèn)一系列class文件(在一個JAR文件中)已經(jīng)由某一實體進行了數(shù)字簽名(有效,可被信賴),并且在簽名過后,這些class文件沒有改動。
雖然版本1.1中發(fā)布的安全API包含了對認(rèn)證的支持,但實際上,除了提供完全信任和完全不信任策略以外,沒有提供許多實際的幫助。
版本1.2提供的API可以幫助建立細(xì)粒度的安全策略,這種策略是在數(shù)字簽名代碼的認(rèn)證基礎(chǔ)上的。
基本沙箱
沙箱模式使你可以接收來自任何來源的代碼,而不是要求用戶避免將來自不信任站點的代碼下載到機器上。
但是當(dāng)來自不可靠來源的代碼運行時,沙箱會限制它進行可能破壞系統(tǒng)的任何動作。
不必指出哪些代碼可以信任,哪些代碼不可以信任,也不必掃描查找病毒,沙箱本身限制了下載的任何病毒或其他惡意的、有漏洞的代碼,使得他它們不能對計算機進行破壞
組成Java沙箱的基本組件如下:
類裝載器結(jié)構(gòu)
class文件檢驗器
內(nèi)置于Java虛擬機(及語言)的安全特性
安全管理器及Java API
Java的沙箱安全模型中的類裝載器和安全管理器是可以由用戶定制的。
類裝載器體系結(jié)構(gòu)
在Java沙箱中,類裝載器體系結(jié)構(gòu)是第一道防線。
1.它防止惡意代碼去干涉善意的代碼
2.它守護了被信任的類庫的邊界
3.它將代碼歸入某類(成為保護域),該類確定了代碼可以進行哪些操作
防止惡意代碼去干涉善意的代碼,通過為不同的類裝載器裝入的類提供不同的命名空間來實現(xiàn)的。
命名空間由一系列唯一的名稱組成,每一個被裝載的類有一個名字,這個命名空間是由Java虛擬機為每一個類裝載器維護的。
命名空間有助于安全的實現(xiàn),有效地在裝入了不同命名空間的類之間設(shè)置一個防護罩。
在Java虛擬機中,在同一個命名空間內(nèi)的類可以直接進行交互,而不同的命名空間中的類甚至不能覺察到彼此的存在,除非顯示的提供了允許它們進行交互的機制。
類裝載器體系結(jié)構(gòu)守護了被信任的類庫的邊界,這是通過分別使用不同的類裝載器裝載可靠的包和不可靠的包來實現(xiàn)的。
雖然通過賦給成員受保護(或包訪問)的訪問限制,可以在同一個包中的類型間授予彼此訪問的特殊權(quán)限,但這種特殊的權(quán)限只能授給在同一個包中的運行時成員,而且它們必須是由同一個裝載器裝載的。
用戶自定義類裝載器經(jīng)常依賴其他類裝載器--至少依賴于虛擬機啟動時創(chuàng)建的啟動類裝載器-來幫助它實現(xiàn)一些類裝載請求。
在版本1.2以前,非啟動類裝載器必須顯示地求助于其他類裝載器,類裝載器可以請求另一個用戶自定義的類裝載器來裝載一個類,這個請求是通過對被請求的用戶自定義類裝載器調(diào)用loadClass()來實現(xiàn)的,也可以通過調(diào)用findSystemClass()來請求啟動類裝載器來裝載類,這是類ClassLoader中的一個靜態(tài)方法。
在版本1.2中,類裝載器請求另一個類裝載器來裝載類型的過程被形式化,稱為雙親委派模式。
從版本1.2開始,除啟動類裝載器以外的每一個類裝載器,都有一個“雙親”類裝載器,在某個特定的類裝載器試圖以常用方式裝載類型以前,它會先默認(rèn)地將這個任務(wù)“委派”給它的雙親--請求它的雙親來裝載這個類型。這個雙親再依次請求它自己的雙親來裝載這個類型。這個委派的過程一直向上繼續(xù),直到達(dá)到啟動類裝載器,通常啟動類裝載器是委派鏈中的最后一個類裝載器。如果一個類裝載器的雙親類裝載器有能力來裝載這個類型,則這個類裝載器返回這個類型。否則,這個類裝載器試圖自己來裝載這個類型。
在版本1.2以前的大多數(shù)虛擬機的實現(xiàn)中,內(nèi)置的類裝載器(原始類裝載器)負(fù)責(zé)在本地裝載可用的class文件。
這些class文件通常包括哪些要運行的Java應(yīng)用程序的class文件,以及這個應(yīng)用程序所需要的任何類庫,這些類庫中包含Java API的基本class文件。
許多實現(xiàn)都是按照類路徑(class path)指明的順序查找目錄和JAR文件
在版本1.2中,裝載本地可用的class文件的工作被分配到多個類裝載器中,原始類裝載器的內(nèi)置的類型裝載器被重新命名為啟動類裝載器,表示它現(xiàn)在只負(fù)責(zé)裝載那些核心Java API的class文件,核心Java API的class文件是用于“啟動”Java虛擬機的class文件。
在版本1.2中,由用戶自定義類裝載器來負(fù)責(zé)其他class文件的裝載,例如用于應(yīng)用程序運行的class文件,用于安裝或下載標(biāo)準(zhǔn)擴展的class文件,在類路徑中發(fā)現(xiàn)的類庫的class文件等。當(dāng)1.2版本的Java虛擬機開始運行時,在應(yīng)用程序啟動以前,它至少會創(chuàng)建一個用戶自定義類裝載器,也可能創(chuàng)建多個。
所以這些類裝載器被連接在一個雙親-孩子的關(guān)系鏈中,關(guān)系鏈的頂端是啟動類裝載器,關(guān)系鏈的末端是“系統(tǒng)類裝載器”,有時也被成為原始類裝載器。
系統(tǒng)類裝載器,指由Java應(yīng)用程序創(chuàng)建的、新的用戶定義類裝載器的默認(rèn)委派雙親,它裝載應(yīng)用程序的初始類。
Java虛擬機只把彼此訪問的特殊權(quán)限授予由同一個類裝載器裝載到同一個包中的類型。
運行時包,它指由同一個類裝載器裝載的、屬于同一個包的、多個類型的集合。
啟動類裝載器裝載核心Java API的class文件,這些class文件是最可信任的。
已安裝擴展的類裝載器裝載來自于任何已安裝擴展的class文件,這個也是可信的。
由類路徑裝載器中發(fā)現(xiàn)的代碼不能訪問已安裝擴展或Java API中的包內(nèi)可見成員。
類裝載器可以簡單地拒絕裝載特定的禁止類型就可以了。
類裝載器必須將每一個被裝載的類放置在一個保護域中,一個保護域定義了這個代碼在運行時將得到怎樣的權(quán)限。
class文件檢測器
class文件實質(zhì)上是一個字節(jié)序列,class文件檢驗器的實現(xiàn)的目標(biāo)之一就是程序的健壯性。
Java虛擬機的class文件校驗器在字節(jié)碼執(zhí)行之前,必須完成大部分檢驗工作。
它只在執(zhí)行前對字節(jié)碼進行一次分析(并檢驗它的完整性),每一次遇到一個跳轉(zhuǎn)指令時都進行檢驗。
虛擬機將確認(rèn)所有的跳轉(zhuǎn)指令會達(dá)到另一條合法的指令,而且這條指令是在這個方法的字節(jié)流中的。
class文件檢驗器要進行四趟獨立的掃描來完成,
第一趟 是在類被裝載時進行的,檢查這個class文件的內(nèi)部結(jié)構(gòu),以保證它可以被安全地編譯。
第二趟+第三趟是在連接過程中進行的,確認(rèn)類型數(shù)據(jù)遵從Java編程語言的語義,包括校驗它所包含的所有字節(jié)碼的完整性。
第四趟 是在進行動態(tài)連接的過程中解析符號引用時進行的,確認(rèn)被引用的類、字段以及方法確實存在。
第一趟:class文件的結(jié)構(gòu)檢查
對每一段將被當(dāng)作類型導(dǎo)入的字節(jié)序列,class文件檢驗器都會確認(rèn)它是否符合Java class文件的基本結(jié)構(gòu)。
例如每個class文件必須以4個同樣的字節(jié)開始:魔數(shù)0xCAFEBABE。這個魔數(shù)的用處是讓class文件分析器很容易分辨出某個文件有明顯問題而加以拒絕。
校驗器還必須確認(rèn)在class文件中聲明的主版本號和次版本號,這個版本號必須在這個Java虛擬機實現(xiàn)可以支持的范圍以內(nèi)。
class文件檢驗器 必須檢驗確認(rèn)這個class文件沒有被刪節(jié),尾部也沒有附帶其他的字節(jié)。
class文件中包含的每一個組成部分都聲明了它的長度和類型,校驗器可以使用組成部分的類型和長度來確定整個class文件的正確的總長度,這樣來檢查一個裝入的文件 其長度是否和它里面的內(nèi)容相一致。
第一趟掃描的主要目的就是保證這個字節(jié)序列正確地定義了一個新類型,它必須遵從Java的class文件的固定格式,這樣才能被編譯成在方法區(qū)中的(基于實現(xiàn)的)內(nèi)部數(shù)據(jù)結(jié)構(gòu)。第二、第三、第四趟掃描 不是在符合class文件格式的二進制數(shù)據(jù)上進行的,而是在方法區(qū)中、由實現(xiàn)決定的數(shù)據(jù)結(jié)構(gòu)上進行的。
第二趟:類型數(shù)據(jù)的語義檢查
在第二趟掃描中,class文件校驗器進行的檢查 不需要查看字節(jié)碼,也不需要查看和裝載任何其他類型。
在這趟掃描中,校驗器查看每個組成部分,確認(rèn)它們是否是其所屬類型的實例,它們的結(jié)構(gòu)是否正確。
例如,方法描述符(它的返回類型,以及參數(shù)的類型和個數(shù))在class文件中被存儲成一個字符串,這個字符串必須符合特定的上下文無關(guān)文法。
校驗器對每個組成部分進行檢查,為了確認(rèn)每個方法描述符都是符合特定語法的、格式正確的字符串。
class文件校驗器檢查這個類本身是否符合特定的條件,它們是由Java編程語言規(guī)定的。
例如,檢驗器強制除object類以外的所有類,都必須有一個超類。
校驗器還要檢查final類沒有被子類化,而且final方法沒有被覆蓋。
還要檢查常量池中的條目是合法的,而且常量池的所有索引必須指向正確類型的常量池條目。
class文件檢驗器在運行時檢查了一些Java語言應(yīng)該在編譯時遵守的強制規(guī)則。
第三趟:字節(jié)碼驗證
在class文件校驗器成功地進行了兩趟檢查后,它將把注意力放在字節(jié)碼上,
Java虛擬機對字節(jié)流進行數(shù)據(jù)流分析,這些字節(jié)流代表的是類的方法。
字節(jié)碼流代表了Java的方法,它是由被稱為操作碼的單字節(jié)指令組成的序列,每一個操作碼后跟著一個或多個操作數(shù)。
操作數(shù)用于在Java虛擬機執(zhí)行操作碼指令時提供所需的額外數(shù)據(jù)。
執(zhí)行字節(jié)碼時,依次執(zhí)行每個操作碼,這就在Java虛擬機內(nèi)構(gòu)成了執(zhí)行的線程。
每一個線程被授予自己的Java棧,這個棧是有不同的棧幀構(gòu)成的。每一個方法調(diào)用將獲得一個自己的棧幀--棧幀其實就是一個內(nèi)存片段,其中存儲著局部變量和計算的中間結(jié)果?
在棧幀中,用于存儲方法的中間結(jié)果的部分被稱為該方法的操作數(shù)棧。操作碼和它得(可選的)操作數(shù)可能存儲在操作數(shù)棧中的數(shù)據(jù),或存儲在方法棧幀中的局部變量中的數(shù)據(jù)。
這樣,在執(zhí)行一個操作碼時,除了可以使用緊隨其后的操作數(shù),虛擬機還可以使用操作數(shù)棧中的數(shù)據(jù),或局部變量中的數(shù)據(jù)或兩者都用。
字節(jié)碼檢驗器要進行大量的檢查,以確保采用任何路徑在字節(jié)碼流中都得到一個確定的操作碼,確保操作數(shù)??偸前_的數(shù)值以及正確的類型。
它必須保證局部變量在賦予合適的值以前不能被訪問,而且類的字段中必須總是被賦予正確類型的值,類的方法被調(diào)用時總是傳遞正確數(shù)值和類型的參數(shù)。
它必須保證每一個操作碼都是合法的,即每一個操作碼都有合法的操作數(shù),以及對每一個操作碼,合適類型的數(shù)值位于局部變量中或是在操作數(shù)粘中。
在第一、第二、第三掃描中,class文件校驗器可以保證導(dǎo)入的class文件構(gòu)成合理,內(nèi)在一致,符合Java編程語言的限制條件,并且包含的字節(jié)碼可以被Java虛擬機安全的執(zhí)行。
第四趟:符號引用的驗證
Java虛擬機將追蹤哪些引用--從被驗證的class文件到被引用的class文件,以確保這個引用是正確的。
必須檢查被檢測的class文件以外的其他類,所以需要裝載新的類。
大多數(shù)Java虛擬機的實現(xiàn)采用延遲裝載類的策略,直到類真正地被程序使用時才裝載。
即使一個實現(xiàn)確實預(yù)先裝載了這些類,為了加快裝載過程的速度,還是會表現(xiàn)為延遲裝載。
class文件檢驗器的第四趟掃描僅僅是動態(tài)連接過程的一部分,
當(dāng)一個class文件被裝載時,它包含了對其他類的符號引用以及它們的字段和方法。
一個符號引用是一個字符串,它給出了名字,并且可能還包含了其他關(guān)于這個被引用項的信息--這些信息必須足矣唯一地識別一個類、字段或方法
對于其他類的符號引用 必須給出這個類的全名;
對于其他類的字段的符號引用必須給出類名、字段名以及字段描述符;
對于其他類中的方法的引用必須給出類名、方法名、方法的描述符
動態(tài)連接是一個將符號引用解析為直接引用的過程
當(dāng)Java虛擬機執(zhí)行字節(jié)碼時,如果它遇到一個操作碼,這個操作碼第一次使用一個指向另一個類的符號引用,那么虛擬機就必須解析這個符號引用。
在解析時,虛擬機執(zhí)行兩個基本任務(wù):
1.查找被引用的類(如果必要的話就裝載它)
2.將符號引用替換為直接引用,例如一個指向類、字段或方法的指針或偏移量
虛擬機必須記住這個直接引用,這樣當(dāng)它以后再次遇到相同的引用時,它就可以立即使用這個直接引用。
當(dāng)Java虛擬機解析一個符號引用時,class文件校驗器的第四趟掃描確保了這個引用是合法的。
當(dāng)這個引用是個非法引用時--例如,這個類不能被裝載,或這個類的確存在,但是不包含被引用的字段或方法--class文件校驗器將會拋出一個錯誤
如果Volcano類中的某個方法調(diào)用了名為Lava的類中的某個方法,這個Lava中的方法的全名和描述符將包含在Volcano的class文件的二進制數(shù)據(jù)中。
當(dāng)Volcano的方法在執(zhí)行過程中第一次調(diào)用Lava的方法時,Java虛擬機必須確認(rèn)類Lava中存在這個方法,并且這個方法的名字和描述符與Volcano中期待的相匹配。
如果這個符號引用(類名、方法名、描述符)是正確的,那么 虛擬機將把他替換為一個直接引用,例如一個指針,從那時開始將使用這個指針。
但如果Volcano類中的符號引用不能匹配Lava類中的任何方法時,第四趟掃描驗證失敗,Java虛擬機將拋出一個NoSuchMethodError
二進制兼容
正因為Java程序是動態(tài)連接的,所以class文件校驗器在第四次掃描中,必須檢查相互引用的類之間是否兼容。
在一個類中,哪些可以被修改、增加和刪除,而并不破壞這個被修改的類與依賴它的那些事先已存在的類之間的二進制兼容性。
例如,向一個類中增加一個新的方法始終是一個影響二進制兼容性的改動,但是不能刪除一個正在被其他類使用的方法。
Java虛擬機內(nèi)置的安全特性
Java虛擬機裝載一個類,并且對它進行了第一到第三趟的class文件檢驗,這些字節(jié)碼就可以被運行。
除了對符合引用的校驗(class文件校驗的第四趟掃描),Java虛擬機在執(zhí)行字節(jié)碼時還進行一些內(nèi)置的安全機制的操作。
這些機制大多數(shù)是Java的類型安全的基礎(chǔ)。
1.類型安全的引用轉(zhuǎn)換
2.結(jié)構(gòu)化的內(nèi)存訪問(無指針?biāo)惴?
3.自動垃圾收集(不必顯示地釋放被分配的內(nèi)存)
4.數(shù)組邊界檢查
5.空引用檢查
通過保證一個Java程序只能使用類型安全的、結(jié)構(gòu)化的方法去訪問內(nèi)存,Java虛擬機使得Java程序更為健壯。
Java虛擬機并未指明運行時數(shù)據(jù)空間在Java虛擬機內(nèi)部是如何分布的。
運行時數(shù)據(jù)空間是指一些內(nèi)存空間,Java虛擬機用這些空間來存儲一個Java程序時所需要的數(shù)據(jù):
Java棧(每個線程一個)、
一個存儲字節(jié)碼的方法區(qū),
以及一個垃圾收集堆(它用來存儲由運行的程序創(chuàng)建的對象)
查看一個class文件的內(nèi)部,將找不到任何內(nèi)存地址。
當(dāng)Java虛擬機裝載一個class文件時,由它決定將這些字節(jié)碼以及其他從class文件中解析得到的數(shù)據(jù)放置在內(nèi)存的什么地方。
當(dāng)Java虛擬機啟動一個線程時,由它決定將它為這個線程創(chuàng)建Java棧放到哪里。
當(dāng)它創(chuàng)建一個新的對象時,也是由它決定這個對象放到內(nèi)存中什么地方。
對每個Java虛擬機的實現(xiàn)來說,由它的設(shè)計者來決定使用什么數(shù)據(jù)結(jié)構(gòu)來表示運行時數(shù)據(jù)空間,并且將它們放在內(nèi)存的哪個位置。
禁止對內(nèi)存進行非結(jié)構(gòu)化訪問,是字節(jié)碼指令集本身的內(nèi)在本質(zhì)。
本地方法沒有經(jīng)過Java API,所以當(dāng)它視圖做些具有破壞性的動作時,安全管理器并未檢查。
安全管理器中包含一個方法,用來確定一個程序是否能裝載動態(tài)連接庫,因為在調(diào)用本地方法時 動態(tài)連接庫是必須的。
在調(diào)用本地方法前必須確認(rèn)它是可信任的。
異常的結(jié)構(gòu)化處理。因為Java虛擬機支持異常,所以當(dāng)一些違反安全的行為發(fā)生時,它會做出一些結(jié)構(gòu)化處理,Java虛擬機將拋出一個異?;蛞粋€錯誤,而不是崩潰。
這個異?;蝈e誤導(dǎo)致這個錯誤線程的死亡,而不是使整個系統(tǒng)陷入癱瘓
拋出一個錯誤 總是導(dǎo)致拋出錯誤的這個線程死亡,這對一個運行的Java程序來說是一個不便因素,但它不會導(dǎo)致整個程序的中止。
如果這個程序還有一些線程正在正常工作,則這些線程有可能繼續(xù)正常工作,即使它的同伴已經(jīng)死亡。
而拋出y一個異??赡軐?dǎo)致這個線程的死亡,但是他經(jīng)常作為一個手段被使用,使程序能夠?qū)⒖刂茝陌l(fā)生異常的地方轉(zhuǎn)到處理這個異常的情況。
安全管理器和Java API
它主要用于保護虛擬機的外部資源不被虛擬機內(nèi)運行的惡意或有漏洞的代碼侵犯。
安全管理器定義了沙箱的外部邊界。
因為它是可定制的,所以它允許程序建立自定義的安全策略。
當(dāng)Java API進行任何可能不安全的操作時,它都會向安全管理器請求許可,從而強制執(zhí)行自定義的安全策略。
要向安全管理器請求許可,Java API將調(diào)用安全管理器對象的"check"方法(因為這些方法名都是以"check"開頭)。
例如,安全管理器的checkRead()方法決定了線程可否讀取一個特定的文件,
checkWrite()方法決定了線程能否對一個特定的文件進行寫操作。
這些方法的實現(xiàn)定義了應(yīng)用程序的定制安全策略。
Java API在進行一個可能不安全的操作前,總是檢查安全管理器。
當(dāng)java應(yīng)用程序啟動時,它還沒有安全管理器,但是,應(yīng)用程序通過將一個指向java.lang.SecurityManager或是其子類的實例傳給setSecurityManager(),依次來安裝安全管理器,這個動作是可選的。
如果應(yīng)用程序沒有安裝安全管理器,那么它就不會對請求Java API的任何動作做限制--Java API將做任何被請求的事(這就是Java應(yīng)用程序在默認(rèn)情況下將不會有任何安全限制的原因)
如果應(yīng)用程序確實安裝了 安全管理器,那么它將負(fù)責(zé)應(yīng)用程序整個剩余的生命周期,它不可被替代、擴展或者修改。
Java API將只執(zhí)行那些被安全管理器同意的請求。