天天看點

Java安全——安全管理器、通路控制器和類裝載器

标簽: java 安全

[toc]

安全管理器在java語言中的作用就是檢查操作是否有權限執行。是java沙箱的基礎元件。我們一般所說的打開沙箱,也是加<code>-djava.security.manager</code>選項。

其實日常的很多api都涉及到安全管理器,它的工作原理一般是:

請求java api

java api使用安全管理器判斷許可權限

通過則順序執行,否則抛出一個exception。

比如在之前的“了解沙箱”這一章提到的,開啟沙箱後,會限制檔案通路,那這個代碼是如何的呢?看下源碼:

比較清晰的api會先去擷取安全管理器,如果開啟沙箱,則安全管理器不是空,檢查checkread(name)。而checkread方法最内層的實作,其實利用了後面要說的通路控制器。

具體點,我們看下securitymanager的主要方法清單:

都是check方法,分别囊括了檔案的讀寫删除和執行、網絡的連接配接和監聽、線程的通路、以及其他包括列印機剪貼闆等系統功能。而這些check代碼也基本橫叉到了所有的核心java api上。

安全管理器可以自定義,作為核心api調用的部分,我們可以自己為自己的業務定制安全管理邏輯。舉個例子如下:

注釋掉代碼中的注釋行,系統列印null,然後正常退出。當我們打開注釋,并且自己擴充一個securitymanager——mysm,它做的事情很簡單,就是覆寫了checkexit方法,在系統退出時抛出一個“no exit”的異常。再執行,結果變成了

顯然,安全管理器生效了。

揭開沙箱面紗,第一步是安全管理器,那麼第二步就是通路控制器了。因為沙箱的所有check方法實作,都是基于accesscontroller的。

要了解accesscontroller,需要了解4個概念:代碼源、權限、政策和保護域。

codesource就是一個簡單的類,用來聲明從哪裡加載類。

permission類是accesscontroller處理的基本實體。permission類本身是抽象的,它的一個執行個體代表一個具體的權限。權限有兩個作用,一個是允許java api完成對某些資源的通路。另一個是可以為自定義權限提供一個範本。權限包含了權限類型、權限名和一組權限操作。具體可以看看basicpermission類的代碼。典型的也可以參看filepermission的實作。

政策是一組權限的總稱,用于确定權限應該用于哪些代碼源。話說回來,代碼源辨別了類的來源,權限聲明了具體的限制。那麼政策就是将二者聯系起來,政策類policy主要的方法就是getpermissions(codesource)和refresh()方法。policy類在老版本中是abstract的,且這兩個方法也是。在jdk1.8中已經不再有abstract方法。這兩個方法也都有了預設實作。

在jvm中,任何情況下隻能安裝一個政策類的執行個體。安裝政策類可以通過policy.setpolicy()方法來進行,也可以通過java.security檔案裡的policy.provider=sun.security.provider.policyfile來進行。jdk1.6以後,policy引入了policyspi,後續的擴充基于spi進行。

保護域可以了解為代碼源和相應權限的一個組合。表示指派給一個代碼源的所有權限。看概念,感覺和政策很像,其實政策要比這個大一點,保護域是一個代碼源的一組權限,而政策是所有的代碼源對應的所有的權限的關系。

jvm中的每一個類都一定屬于且僅屬于一個保護域,這由classloader在define class的時候決定。但不是每個classloader都有相應的保護域,核心java api的classloader就沒有指定保護域,可以了解為屬于系統保護域。

了解了組成,再回頭看accesscontroller。這是一個無法執行個體化的類——僅僅可以使用其static方法。accesscontroller最重要的方法就是checkpermission()方法,作用是基于已經安裝的policy對象,能否得到某個權限。

回到[了解沙箱]()那一篇文章裡的例子,fileinputstream的構造方法就利用securitymanager來checkread。而securitymanager的checkread方法則

這樣來檢查權限。

然而,accesscontroller的使用還是重度關聯類加載器的。如果都是一個類加載器且都從一個保護域加載類,那麼你構造的checkpermission的方法将正常傳回。

當使用了其他類加載器或者使用了java擴充包時,這種情況比較普遍。accesscontroller另一個比較實用的功能是doprivilege(授權)。假設一個保護域a有讀檔案的權限,另一個保護域b沒有。那麼通過accesscontroller.doprivileged方法,可以将該權限臨時授予b保護域的類。而這種授權是單向的。也就是說,它可以為調用它的代碼授權,但是不能為它調用的代碼授權。

classloader對安全模型有三方面的影響:第一,可以結合jvm定義名稱空間,以保護java語言本身安全特性的完整性。第二,在必要時調用securitymanager保證代碼在定義或者通路類時有适當的權限。第三,建立了權限與類對象之間的映射,這樣accesscontroller就知道哪些類擁有哪些權限了。而這可以繞過建立自定義policy類,通過自定義classloader并在其中定義類權限而實作。

先來說說名稱空間,其實就是包名,但是不同的是,不同的classloader可以裝在相同包名的類,而這時,其實對于每個classloader,有一個自己的名字空間。為啥這麼幹?顯然啊,就不說包沖突這事了,從安全角度看,你冒名頂替個com.sun.xx咋辦?肯定得按照classloader來分。從不同網站加載的applet類,就是不同的classloader來做。

類加載器是個層次結構,最基礎的是系統類加載器,下面有很多子類比如urlclassloader。加載一個類時,以委托的形式逐層詢問——總結來就一句話:父親優先。爹能加載,爹先來,不行再由兒子上。一旦為一個域的類定義類加載器,那麼其他域的類加載器的整個祖先鍊路上不包含對應域,也就隔離了彼此的類加載。

總的來說,需要完成以下工作:

1, 詢問安全管理器是否允許通路目前處理的類。如果不行,抛一個安全異常。這一步可選,一般在loadclass()方法開始處實作。對應accessclassinpackage權限。

2, 如果類裝載器已經載入了此類,它将尋找以前定義的類對象,并傳回該對象。這一步在loadclass()内部實作。

3,否則,類裝載器将詢問其父親,遞歸檢視父類裝載器是否知道如何載入此類。是以總會是系統類加載器最先加載,進而避免了核心java api中的類被其他自定義的類冒充。這一步也在loadclass()裡實作。

2和3對應代碼如下

4,詢問安全管理器是否允許程式建立目前處理的類。如果不行,則抛出一個安全異常。這一步可選,如果實作,則需要在findclass()的開始處完成。這一步不是在操作開始時完成,而是在詢問父類裝載器之後進行。這一步對應為defineclassinpackage權限。

5,向一個位元組數組中讀入類檔案。讀取檔案以及建立位元組數組的方式因類加載器不同而不同。在findclass()中完成。

6,為該類建立合适的保護域。保護域可以來自預設安全模型(即從政策檔案中得到),也可以由類加載器擴充。還有一種方法是可以建立一個代碼源對象,并采用其保護域定義。這一步也在findclass()中完成。

7,在findclass()方法中,通過調用defineclass()方法,可以由位元組碼構造一個class對象。如果使用的是第6步中的代碼源,則需要調用getpermissions()方法查找與代碼源相關的權限。defineclass()方法還保證了位元組碼必須通過位元組碼校驗器的檢查。

8,最後還需要解析該類。即它所直接引用的類也應由目前類加載器找到。隻有直接引用的才算,作為執行個體變量、方法參數或局部變量來使用的類不算。這一步在loadclass()中完成。對應上面代碼中的resovleclass()。

1,對應的代碼如下:

urlclassloader的newinstance()方法會構造一個内部的工廠加載器類。這個類的loadclass()方法做了checkpackageaccess的事情。

2,3 兩步與超級父類classloader相同。就是上面classloader的loadclass()做的事情。

4,5,6,7 四個步驟涉及到findclass()方法,urlclassloader覆寫了findclass()方法,但是最新版的jdk,其實将這幾個步驟做的事情都在defineclass()裡做掉了。裡面的邏輯實作如下:

defineclass()的邏輯:

而這裡面所有的defineclass都在classloader這個超級父類裡做了實作。

通常的java的安全性,都是從類加載器、安全管理器和通路控制器之間的關系考慮的。一般來說類加載器的作用更重要。

如果需要靈活的安全政策,往往要自定義類加載器。自定義類加載器允許在定義類時調整安全政策。這與實作一個新的policy類相似。一般認為自定義類加載器會比修改一個policy類要容易。