Java API中有很多都使用了SecurityManager,這到底是什么玩意?最近看公司的產品的源碼,也有不少SecurityManager、AccessControlContext等相關的代碼,只是知道它們與安全有關,但是它們到底是怎么一回事呢?Spring也有一個Security框架,與Java Security有什么關聯呢?另外有經驗的開發人員調試程序時可能會查看ProtectionDomain、CodeSource,這兩者又是什么呢?
Java Sandbox
提到Java Security,就不得不說Java Sandbox模型。
Java2 Security Model:
Java2平台上,加載類時,會形成不同的sandbox,同時也會根據相關的security policy,為這些sandbox生成不同的安全策略,這些安全策略會在應用程序執行時,進行檢查,以保護資源被惡意的操作。
這張圖指出了Java應用程序的真實的執行過程。
1) 編譯期強制規則驗證,而后生成class file
Java的強制性規則有:
A: private, protected, default, public 。這個都知道,是關系到可見性,是對應用程序中內存資源的保護。
B: final的變量初始化后不能被改變
C: 變量要先初始化后使用
以及一些其他的規則,通過這些規則驗證后,就生成class file,也就是常說的字節碼文件。
2)ClassLoader加載class file后定義類生成Class對象
類加載器也是一道坎,不是說你讓它加載,它就加載的,它也是要進行驗證的。
假如駭客寫了一些java文件編譯后放到classpath目錄下,或者是將jdk中自帶某些核心API反編譯后進行某些修改,覆蓋原有文件,這樣對程序的危害可以極大的。所以類加載時,也是有必要進行檢查的。
從這張圖片可以看出在類加載器定義類的過程也會對字節碼進行檢查的,下面可以看一下ClassLoader中defineClass的過程:
protected final Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain) throws ClassFormatError { // 檢查類加載器是否初始化 check(); // 形成為該類生成protectionDomain和codesource protectionDomain = preDefineClass(name, protectionDomain); Class c = null; String source = defineClassSourceLocation(protectionDomain); // 真實的定義Class的過程,這個方法是native的,字節碼檢查的過程也是這里進行的,這里是看不到的,也是不能讓我們看到的,如果我們可見,就可以自定義,這樣檢查就形同虛設。 try { c = defineClass1(name, b, off, len, protectionDomain, source); } catch (ClassFormatError cfe) { c = defineTransformedClass(name, b, off, len, protectionDomain, cfe, source); } // 完善證書等信息 postDefineClass(c, protectionDomain); return c; }
既然代碼層面,看不到如何檢查字節碼的,那么至少可以來了解一下,到底做了哪些檢查呢?
D:檢查class file的格式是否正確,JVM Specification 中說明了class file的格式,感興趣的話可以到官網下載看看。例如:class file要有正確的長度、魔數 等。
魔數用於確定文件類型,UNIX系統不是根據擴展名來確定文件類型的,就是根據這個魔數來的。想要知道class file的魔數、以及是怎么定義的,可以參考《深入理解Java虛擬機》。
E: final的類沒有子類
F: 原生類型的數據有無不合法的類型轉換(E.G.: int to Object)
G: 引用類型的數據有無不合法的類型轉換,例如將父類對象轉換為子類類型。
H: 有沒有操作數出現棧溢出現象
等。
其實還有兩種檢查,這兩種是在運行時進行的:
I: 數組不能越界
J: 數據不能強制轉化為其他不相干的類型
在定義類的過程中,還產生了與這個類相關聯的ProtectionDomain。Java Security模塊的設計如下圖所示。
但並不是所有的ClassLoader都會生成ProtectionDomain。例如我前之前的一篇博客中定義的那個類加載器,又或者時bootstrapClassLoader。 只有繼承了SecurClassLoader的ClassLoader在defineClass時都會生成相關聯的ProtectionDomain, 一般情況下我們自定義ClassLoader時都會繼承UrlClassLoader,而UrlClassLoader又繼承了SecurClassLoader,所以我們定義的ClassLoader在執行defineClass時一般都會生成ProtectionDomain。
ProtectionDomain的設計模型是很重要的,接下來要說的AccessController和SecurityManager都是在ProtectionDomain的基礎上才有所作為的。所以ProtectionDomain就在類加載時就確立。
默認情況下,一個jar包就對應一個ProtectionDomain。
網上關於Java Security方面的教程,說的最多莫過於Policy了,因為它是配置安全策略的。我們可能不會去定義Permission(Java中定義的Permission已經夠我們使用),但是我們不可或缺的要去配置安全策略,來使用這些Permission為我們服務。
3)應用程序訪問相關資源
3.1 SecurityManager#checkPermission()
Java提供了安全模型,我們在程序中如何使用呢?
一般來說都是通過SecurityManager來完成的,使用方式為:
SecurityManager sm = getSecurityManager(); if (sm != null) { // sm.checkPermission(); }
例如:
System.getProperty(String key)
public static String getProperty(String key) { checkKey(key); SecurityManager sm = getSecurityManager(); if (sm != null) { sm.checkPropertyAccess(key); } return props.getProperty(key); }
例如:
public FileInputStream(File file) throws FileNotFoundException { String name = (file != null ? file.getPath() : null); SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkRead(name); } if (name == null) { throw new NullPointerException(); } fd = new FileDescriptor(); open(name); }
默認情況下,我們的程序並沒有開啟Java的安全策略。想要看看開啟安全策略后你的應用程序會是什么樣的,可以使用JVM參數:-Djava.security.manager 。
如果要使用代碼來開啟,可以使用System.setSecurityManager(securitymanager)來啟動。
在代碼中只要像上面那樣簡單的寫上兩三行代碼就可以檢查相應的權限了。那么它們的執行過程是什么呢?
SecurityManager中所有檢查權限相關的方法都會調用SecurityManager的checkPermission方法,下面的這個時序圖說明了SecurityManager#checkPermission(Permission)的執行過程。
從這個圖上也能看到最后還是Permission#implies起作用的。
3.2 AccessController.doPrivileged()
有時我們還會在代碼中看到使用AccessController.doPrivileged()方法的,這個又是做什么呢?
假設有下列一個應用場景:有一個ProtectionDomain的CodeSource是com目錄,在它下面有三個目錄:core,moduleA,web,在這個 ProtectionDomain中,對所有的文件都有read權限,只有web目錄下的resource目錄下的文件,可以有write權限。現在有一需求,要在core目錄下的某個文件有write權限。
/com |--core |--moduleA |--web |--bean |--service |--dao |--resource
我們的程序中肯定會這樣寫:new FileOutputStream(File file)。上面已經粘出來FileInputStream(File file)實現過程。也就是說檢查對該文件有無讀權限。那么對應的FileOutputStream中肯定也會有檢查是否有寫權限的過程。上面的描述中已經知道,對於core下沒有寫權限的,所以我們的需求是無法滿足的。那怎么辦呢?
AccessController.doPrivileged()就可以幫肋完成上述任務。
FileOutputStream fos=null; String filepath=”./com/core/xx”; fos=AccessController.doPrivileged(new PriviliegedAction(){ public FileOutputStream run(){ return new FileOutputStream(filepath); } }); if(fos!=null){ // xxxxxxxx }
這到底是怎么回事呢?下面貼Java API中AccessController描述中的一段話:
A caller can be marked as being "privileged" (see doPrivileged and below). When making access control decisions, the checkPermission method stops checking if it reaches a caller that was marked as "privileged" via a doPrivileged call without a context argument (see below for information about a context argument). If that caller's domain has the specified permission, no further checking is done and checkPermission returns quietly, indicating that the requested access is allowed. If that domain does not have the specified permission, an exception is thrown, as usual.
這段話大意就是說:
如果使用了doPrivileged方法將調用者標記為privileged,在執行AccessController.checkPermission()做檢查時,當檢查到這個調用者時,就會終止檢查,然后只作一個判斷:如果caller所在的域有指定的權限就可以了。
SecurityManager#checkPermission實際上就是調用了AccessController.checkPermission(),所以這個解決方案對於SecurityManager#checkPermission也是適用的。
就暫說到這里吧,Java Security還有很多細節的東西沒有提到。本文只是對Java Security有了一個整體結構上的說明。以及一些常用代碼的解釋,看完這篇文章,相信以往對一些有疑惑的代碼,現在也應該可以明白七八分了。