作為一種誕生于互聯(lián)網(wǎng)興起時(shí)代的語(yǔ)言,Java 從一開(kāi)始就帶有安全上的考慮,如何保證通過(guò)互聯(lián)網(wǎng)下載到本地的 Java 程序是安全的,如何對(duì)Java 程序訪問(wèn)本地資源權(quán)限進(jìn)行有限授權(quán),這些安全角度的考慮一開(kāi)始就影響到 Java 語(yǔ)言的設(shè)計(jì)與實(shí)現(xiàn)。可以說(shuō) Java在這些方面的探索與經(jīng)驗(yàn),對(duì)后來(lái)的一些語(yǔ)言與產(chǎn)品都帶來(lái)了積極影響。本篇文章中將介紹 Java 中安全模型,以及如何利用安全訪問(wèn)控制機(jī)制來(lái)實(shí)現(xiàn)一些特定目的。
Java 中的安全模型
在Java中將執(zhí)行程序分成本地和遠(yuǎn)程兩種,本地代碼默認(rèn)視為可信任的,而遠(yuǎn)程代碼則被看作是不受信的。對(duì)于授信的本地代碼,可以訪問(wèn)一切本地資源。而對(duì)于非授信的
遠(yuǎn)程代碼在早期的 Java 實(shí)現(xiàn)中,安全依賴于沙箱 (Sandbox) 機(jī)制。沙箱機(jī)制就是將 Java 代碼限定在虛擬機(jī) (JVM)
特定的運(yùn)行范圍中,并且嚴(yán)格限制代碼對(duì)本地系統(tǒng)的資源訪問(wèn),通過(guò)這樣的措施來(lái)保證對(duì)遠(yuǎn)程代碼的有效隔離,防止對(duì)本地系統(tǒng)造成破壞。如圖 1 所示,
圖 1.JDK1.0 安全模型
[圖片上傳中。。。(1)]
但如此嚴(yán)格的安全機(jī)制也給程序的功能擴(kuò)展帶來(lái)障礙,比如當(dāng)用戶希望遠(yuǎn)程代碼訪問(wèn)本地系統(tǒng)的文件時(shí)候,就無(wú)法實(shí)現(xiàn)。因此在后續(xù)的 Java1.1 版本中,針對(duì)安全機(jī)制做了改進(jìn),增加了安全策略,允許用戶指定代碼對(duì)本地資源的訪問(wèn)權(quán)限。如圖 2 所示,
圖 2.JDK1.1 安全模型
[圖片上傳中。。。(2)]
在 Java1.2 版本中,再次改進(jìn)了安全機(jī)制,增加了代碼簽名。不論本地代碼或是遠(yuǎn)程代碼,都會(huì)按照用戶的安全策略設(shè)定,由類加載器加載到虛擬機(jī)中權(quán)限不同的運(yùn)行空間,來(lái)實(shí)現(xiàn)差異化的代碼執(zhí)行權(quán)限控制。如圖 3 所示,
圖 3.JDK1.2 安全模型
當(dāng)前最新的安全機(jī)制實(shí)現(xiàn),則引入了域 (Domain)
的概念。虛擬機(jī)會(huì)把所有代碼加載到不同的系統(tǒng)域和應(yīng)用域,系統(tǒng)域部分專門負(fù)責(zé)與關(guān)鍵資源進(jìn)行交互,而各個(gè)應(yīng)用域部分則通過(guò)系統(tǒng)域的部分代理來(lái)對(duì)各種需要的
資源進(jìn)行訪問(wèn)。虛擬機(jī)中不同的受保護(hù)域 (Protected Domain),對(duì)應(yīng)不一樣的權(quán)限
(Permission)。存在于不同域中的類文件就具有了當(dāng)前域的全部權(quán)限,如圖 4 所示:
圖 4. 最新安全模型
[圖片上傳中。。。(4)]
以上提到的都是基本的 Java 安全模型概念,在應(yīng)用開(kāi)發(fā)中還有一些關(guān)于安全的復(fù)雜用法,其中最常用到的 API 就是
doPrivileged。doPrivileged
方法能夠使一段受信任代碼獲得更大的權(quán)限,甚至比調(diào)用它的應(yīng)用程序還要多,可做到臨時(shí)訪問(wèn)更多的資源。有時(shí)候這是非常必要的,可以應(yīng)付一些特殊的應(yīng)用場(chǎng)
景。例如,應(yīng)用程序可能無(wú)法直接訪問(wèn)某些系統(tǒng)資源,但這樣的應(yīng)用程序必須得到這些資源才能夠完成功能。針對(duì)這種情況,Java SDK 給域提供了
doPrivileged 方法,讓程序突破當(dāng)前域權(quán)限限制,臨時(shí)擴(kuò)大訪問(wèn)權(quán)限。下面內(nèi)容會(huì)詳細(xì)講解一下安全相關(guān)的方法使用。
Java 安全控制實(shí)現(xiàn)
JavaSDK 中與安全相關(guān)的類和接口都放在 java.security包中,其中既包括訪問(wèn)控制配置及細(xì)粒度訪問(wèn)控制框架的實(shí)現(xiàn),還包括簽名和加解密的實(shí)現(xiàn)。本文中涉及到的安全訪問(wèn)控制主要與安全包中訪問(wèn)控制框架相關(guān),這里面最常用的就是 AccessContorller 類。通過(guò)下圖的描述,您可以了解 ACC(Access Contorller)機(jī)制是如何運(yùn)作的。
在某一個(gè)線程的調(diào)用棧中,當(dāng) AccessController 的 checkPermission 方法被最近的調(diào)用程序(例如 A 類中的方法)調(diào)用時(shí),對(duì)于程序要求的所有訪問(wèn)權(quán)限,ACC 決定是否授權(quán)的基本算法如下:
- 如果調(diào)用鏈中的某個(gè)調(diào)用程序沒(méi)有所需的權(quán)限,將拋出 AccessControlException;
- 若是滿足以下情況即被授予權(quán)限:
a. 調(diào)用程序訪問(wèn)另一個(gè)有該權(quán)限域里程序的方法,并且此方法標(biāo)記為有訪問(wèn)“特權(quán)”;
b. 調(diào)用程序所調(diào)用(直接或間接)的后續(xù)對(duì)象都有上述權(quán)限。
在上面例子的調(diào)用鏈中,假定 E 域和 F 域不具備 X 權(quán)限 (permission),而在 C.class 對(duì)應(yīng)的 G 域具有 X 權(quán)限,同時(shí) C使用 X 權(quán)限的對(duì)外接口 Y 方法是通過(guò) doPrivilege 方式實(shí)現(xiàn)。那么,B.class A.class 調(diào)用 Y 方法就都具備 X權(quán)限。如果 Y 方法沒(méi)有標(biāo)注 doPrivilege,那么對(duì) Y 方法的調(diào)用就不具備 X 權(quán)限。
還有一種特殊的情況,就是訪問(wèn)控制上下文的繼承問(wèn)題。當(dāng)一個(gè)線程創(chuàng)建另一個(gè)新線程時(shí),會(huì)同時(shí)創(chuàng)建新的堆棧。如果創(chuàng)建新線程時(shí)沒(méi)有保留當(dāng)前的安全上下文,也就是線程相關(guān)的安全信息,則新線程調(diào)用 AccessController.checkPermission檢驗(yàn)權(quán)限時(shí),安全訪問(wèn)控制機(jī)制只會(huì)根據(jù)新線程的上下文來(lái)決定安全性問(wèn)題,而不會(huì)考慮其父線程的相應(yīng)權(quán)限。這個(gè)清除堆棧的做法本身并不會(huì)給系統(tǒng)帶來(lái)安全隱患,但它會(huì)使源代碼,尤其是系統(tǒng)代碼的編寫(xiě)容易出現(xiàn)錯(cuò)誤。例如,對(duì)安全框架實(shí)現(xiàn)不熟悉編程人員可能會(huì)很自然地認(rèn)為,子線程執(zhí)行的信任代碼繼承了父線程執(zhí)行的不可信任代碼的安全限制特性。當(dāng)從子線程內(nèi)訪問(wèn)受控制的資源時(shí),如果父線程的安全上下文信息并未保存,就會(huì)導(dǎo)致意外的安全漏洞。因?yàn)閬G失的父線程中安全限制數(shù)據(jù)會(huì)使子線程將資源傳遞給一些不可信任的代碼。因此,在創(chuàng)建新線程時(shí),必須確保利用父線程創(chuàng)建,或利用其他形式創(chuàng)建代碼。總之,要保證讓子線程自動(dòng)繼承父線程的安全性上下文,這樣子線程中的后續(xù) AccessController.checkPermission調(diào)用就會(huì)考慮所繼承的父線程的安全特性。
需要注意是 AccessController 類的 checkPermission方法將在當(dāng)前執(zhí)行線程的上下文,包括繼承的上下文中進(jìn)行安全檢查。當(dāng)這種安全檢查只能在不同的上下文中進(jìn)行時(shí)就會(huì)出現(xiàn)問(wèn)題。意即,本應(yīng)在一個(gè)線程上下文內(nèi)部進(jìn)行的安全檢查,有時(shí)卻需要在不同上下文中進(jìn)行。例如,當(dāng)一個(gè)線程將某個(gè)事件傳給另一個(gè)線程時(shí),如果所請(qǐng)求的事件服務(wù)要求訪問(wèn)某種安全受控資源,則為其請(qǐng)求事件服務(wù)的第二個(gè)線程將沒(méi)有事件產(chǎn)生源線程相應(yīng)的上下文來(lái)完成所需的訪問(wèn)控制決策。為解決這樣的問(wèn)題,Java 在AccessController 類中提供了 getContext 方法和 AccessControlContext 對(duì)象。通過(guò)getContext 方法可獲取當(dāng)前調(diào)用上下文的“快照 (snapshot)”,然后將其存放到返回的 AccessControlContext對(duì)象中。調(diào)用的樣例程序如下所示:AccessControlContext acc =AccessController.getContext();
getContext方法將當(dāng)前上下文的快照信息捕獲,然后執(zhí)行程序就可以通過(guò)檢查前后不同上下文中的信息,即比較快照上下文信息與本上下文信息,然后來(lái)做出對(duì)受控資源訪問(wèn)控制的決策。上面問(wèn)題就可以如下方式來(lái)解決,當(dāng)前一個(gè)線程把某個(gè)請(qǐng)求事件傳給第二個(gè)線程時(shí),同時(shí)捕獲其上下文信息并將這些信息提供給后一個(gè)線程。略有不同的是,AccessControlContext 類本身的 checkPermission方法可根據(jù)它自身攜帶的上下文信息來(lái)決定訪問(wèn)控制,而不是根據(jù)當(dāng)前正在執(zhí)行的線程上下文。因此必要時(shí),后一個(gè)線程可直接通過(guò)調(diào)用前一個(gè)線程上下文快照本身的權(quán)限檢查方法來(lái)執(zhí)行相應(yīng)的安全檢查。如下,acc.checkPermission(permission),上述方法調(diào)用等同于在前一個(gè)線程的上下文中執(zhí)行相同的安全檢查,盡管訪問(wèn)控制檢查實(shí)際上是在后一個(gè)線程中完成的。
安全控制使用的代碼實(shí)例
上面關(guān)于安全控制使用的描述還是比較晦澀,下面將通過(guò)一個(gè)代碼示例進(jìn)行說(shuō)明。
在Eclipse 開(kāi)發(fā)環(huán)境中建立兩個(gè)不同工程:projectX 和 projectY。我們會(huì)給 projectX 工程中的 bin目錄賦予寫(xiě)文件的權(quán)限,換句話說(shuō)就是允許所有存在于此目錄中的 class 文件可以自由的在 bin 目錄中進(jìn)行文件寫(xiě)操作。然后,我們會(huì)在projectY 工程中調(diào)用 projectX工程中的一個(gè)文件操作工具類。這個(gè)工具類提供兩種類型接口,一種是特權(quán)訪問(wèn)方式,另外一種是普通訪問(wèn)方式。由于在 projectY工程中的文件是不具備在 projectX 工程中 bin 目錄的任何寫(xiě)權(quán)限,所以通過(guò)三種不同訪問(wèn)方式的調(diào)用結(jié)果,我們就可以很清楚地了解到Java 中安全控制該如何使用。
假定 ProjectX 的項(xiàng)目路徑為 D:\workspace\projectX\
package learn.java.security;
import java.io.File;
import java.io.IOException;
import java.security.AccessControlException;
import java.security.AccessController;
import java.security.PrivilegedAction;
public class FileUtil {
// 工程 A 執(zhí)行文件的路徑
private final static String FOLDER_PATH = "D:\\workspace\\projectX\\bin";
public static void makeFile(String fileName) {
try {
// 嘗試在工程 A 執(zhí)行文件的路徑中創(chuàng)建一個(gè)新文件
File fs = new File(FOLDER_PATH + "\\" + fileName);
fs.createNewFile();
} catch (AccessControlException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void doPrivilegedAction(final String fileName) {
// 用特權(quán)訪問(wèn)方式創(chuàng)建文件
AccessController.doPrivileged(new PrivilegedAction() {
@Override
public String run() {
makeFile(fileName);
return null;
}
});
}
}
假定 ProjectY 的項(xiàng)目路徑為 D:\workspace\projectY\
package demo.security;
import java.io.File;
import java.io.IOException;
import java.security.AccessControlException;
import learn.java.security.FileUtil;
public class DemoDoPrivilege {
public static void main(String[] args) {
System.out.println("***************************************");
System.out.println("I will show AccessControl functionality...");
System.out.println("Preparation step : turn on system permission check...");
// 打開(kāi)系統(tǒng)安全權(quán)限檢查開(kāi)關(guān)
System.setSecurityManager(new SecurityManager());
System.out.println();
System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
System.out.println("
Create a new file named temp1.txt via privileged action ...");
// 用特權(quán)訪問(wèn)方式在工程 A 執(zhí)行文件路徑中創(chuàng)建 temp1.txt 文件
FileUtil.doPrivilegedAction("temp1.txt");
System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
System.out.println();
System.out.println("http://///////////////////////////////////////");
System.out.println("Create a new file named temp2.txt via File ...");
try {
// 用普通文件操作方式在工程 A 執(zhí)行文件路徑中創(chuàng)建 temp2.txt 文件
File fs = new File(
"D:\\workspace\\projectX\\bin\\temp2.txt");
fs.createNewFile();
} catch (IOException e) {
e.printStackTrace();
} catch (AccessControlException e1) {
e1.printStackTrace();
}
System.out.println("http://///////////////////////////////////////");
System.out.println();
System.out.println("-----------------------------------------");
System.out.println("create a new file named temp3.txt via FileUtil ...");
// 直接調(diào)用普通接口方式在工程 A 執(zhí)行文件路徑中創(chuàng)建 temp3.txt 文件
FileUtil.makeFile("temp3.txt");
System.out.println("-----------------------------------------");
System.out.println();
System.out.println("***************************************");
}
}
應(yīng)用的安全訪問(wèn)控制策略文件 (MyPolicy.txt) 如下 , 假定安全策略文件放于 projectY 工程的根目錄下:
// 授權(quán)工程 A 執(zhí)行文件路徑中文件在本目錄中的寫(xiě)文件權(quán)限
<code>
grant codebase "file:/D:/workspace/projectX/bin"
{
permission java.io.FilePermission
"D:\workspace\projectX\bin\*", "write";
};
</code>
下面就可以運(yùn)行程序了,您可以選擇在 Eclipse 開(kāi)發(fā)環(huán)境中直接運(yùn)行,也可以通過(guò)命令行來(lái)執(zhí)行。命令行執(zhí)行如下所示,假定當(dāng)前執(zhí)行目錄就是 projectY 的根目錄。
<code>
java -Djava.security.policy=.\MyPolicy.txt -classpath
D:\workspace\projectY\bin;D:\workspace\projectX\bin demo.security.DemoDoPrivilege
執(zhí)行結(jié)果如下:
</code>
***************************************
I will show AccessControl functionality...
Preparation step : turn on system permission check...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Create a new file named temp1.txt via privileged action ...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
////////////////////////////////////////
Create a new file named temp2.txt via File ...
java.security.AccessControlException: Access denied (java.io.FilePermission
D:\workspace\projectX\bin\temp2.txt write)
at java.security.AccessController.checkPermission(AccessController.java:108)
at java.lang.SecurityManager.checkPermission(SecurityManager.java:533)
at java.lang.SecurityManager.checkWrite(SecurityManager.java:963)
at java.io.File.createNewFile(File.java:882)
at demo.security.DemoDoPrivilege.main(DemoDoPrivilege.java:32)
////////////////////////////////////////
----------------------------------------
create a new file named temp3.txt via FileUtil ...
java.security.AccessControlException: Access denied (java.io.FilePermission
D:\workspace\projectX\bin\temp3.txt write)
at java.security.AccessController.checkPermission(AccessController.java:108)
at java.lang.SecurityManager.checkPermission(SecurityManager.java:533)
at java.lang.SecurityManager.checkWrite(SecurityManager.java:963)
at java.io.File.createNewFile(File.java:882)
at learn.java.security.FileUtil.makeFile(FileUtil.java:16)
at demo.security.DemoDoPrivilege.main(DemoDoPrivilege.java:43)
----------------------------------------
***************************************
通過(guò)程序打印結(jié)果來(lái)看,
當(dāng)往projectX 工程中 bin 目錄創(chuàng)建 temp1.txt,temp2.txt,temp3.txt
文件時(shí)候,除了通過(guò)特權(quán)訪問(wèn)方式可以創(chuàng)建成功外,通過(guò)普通接口訪問(wèn)或者直接文件操作方式都會(huì)失敗,失敗的原因都是沒(méi)有通過(guò)權(quán)限檢查。對(duì)照前文所描述的權(quán)限檢查規(guī)則,用一句話總結(jié)就是想訪問(wèn)安全資源,要么調(diào)用鏈上權(quán)限齊全,要么就要用特權(quán)。特權(quán)訪問(wèn)機(jī)制實(shí)際上就是給應(yīng)用開(kāi)后門的使用上需要小心,所以這也給代碼實(shí)現(xiàn)帶來(lái)新的考慮,開(kāi)放范圍一定要限定好,否則可能留下安全隱患。
文章轉(zhuǎn)至:http://www.ibm.com/developerworks/cn/java/j-lo-javasecurity/