Java安全:SecurityManager與AccessController

Mr_小白發表於2018-08-16

前言

什麼是安全?

  • 程式不能惡意破壞使用者計算機的環境,比如特洛伊木馬等可自我進行復制的惡意程式。
  • 程式不可獲取主機及其所在網路的私密資訊。
  • 程式的提供者和使用者的身份需要通過特殊驗證。
  • 程式所涉及的資料在傳輸、持久化後都應是被加密的。
  • 程式的操作有相關規則限制,並且不能耗費過多的系統資源。

保護計算機上的資訊不被非法獲取和修改時Java最初的,也是最基本的設計目標,但同時還要保證Java程式在主機上的執行不受影響。

Java安全方面的支援

JDK本身提供了基本的安全方面的功能,比如可配置的安全策略、生成訊息摘要、生成數字簽名等等。同時,Java也有一些擴充套件程式,更加全面地支撐了整個安全體系。

Java加密擴充套件包(JCE)提供了密碼、安全金鑰交換、安全訊息摘要、金鑰管理系統等功能。

Java安全套接字擴充套件包(JSSE)提供了SSL(安全套接字層)的加密功能,保證了與SSL伺服器或SSL客戶的通訊安全。

Java鑑別與授權服務(JAAS)可以在Java平臺上提供使用者鑑別,並且允許開發者根據使用者提供的鑑別信任狀准許或拒絕使用者對程式的訪問。

一、Java沙箱

如何理解?程式要在主機上安裝,那麼主機必須為該程式提供一個執行的場所(執行環境),該場所支援程式執行的同時,也限制其可以獲取的資源。就好比小孩子去你家玩,你需要提供一個空間讓她玩耍且不會受傷,同時還要保證你女朋友新買的化妝鏡不會被孩子打碎。

Java沙箱負責保護一些系統資源,而且保護級別是不同的。

  • 內部資源,如本地記憶體;
  • 外部資源,如訪問其檔案系統或是在同一區域網的其他機器;
  • 對於執行的元件(applet),可以訪問其web伺服器;
  • 主機通過網路傳輸到磁碟的資料流。

一般來講,沙箱的預設狀態允許其中的程式訪問CPU、記憶體等資源,以及其上裝在的Web伺服器。若沙箱完全開放,則其中程式的許可權與主機相同。

當前最新的安全機制實現,則引入了域 (Domain) 的概念,可以理解為將沙箱細分為多個具體的小沙箱。虛擬機器會把所有程式碼載入到不同的系統域和應用域,系統域部分專門負責與關鍵資源進行互動,而各個應用域部分則通過系統域的部分代理來對各種需要的資源進行訪問。虛擬機器中不同的受保護域 (Protected Domain),對應不一樣的許可權 (Permission)。存在於不同域中的類檔案就具有了當前域的全部許可權,如下圖所示:

Java安全:SecurityManager與AccessController

沙箱的實現取決於下面三方面的內容:

  • 安全管理器,利用其提供的機制,可以使Java API確定與安全相關的操作是否允許執行。
  • 存取控制器,安全管理器預設實現的基礎。
  • 類裝載器,可以實現安全策略和類的封裝。

從Java API的角度去看,應用程式的安全策略是由安全管理器去管理的。安全管理器決定應用是否可以執行某項操作。這些操作具體是否可以執行的依據,是看其能否對一些比較重要的系統資源進行訪問,而這項驗證由存取控制器進行管控。這麼看來,存取控制器是安全管理器的基礎實現,安全管理器能做的,存取控制器也可以做。那麼問題來了,為什麼還需要安全管理器?

Java2以前是沒有存取控制器的,那個時候安全管理器利用其內部邏輯決定應用的安全策略,若要調整安全策略,必須修改安全管理器本身。Java2開始,安全管理器將這些工作交由存取控制器,存取控制器可以利用策略檔案靈活地指定安全策略,同時還提供了一個更簡單的方法,實現了更細粒度地將特定許可權授予特定的類。因此,Java2之前的程式都是利用安全管理器的介面實現系統安全的,這意味著安全管理器是不能修改的,那麼引入的存取控制器並不能完全替代安全管理器。兩者的關係如下圖:

Java安全:SecurityManager與AccessController

二、 安全管理器

安全管理器是Java API和應用程式之間的“第三方權威機構”。好比貸款時,銀行會根據央行徵信系統查詢使用者的信用情況決定是否放款。Java應用程式請求Java API完成某個操作,Java API會向安全管理器詢問是否可以執行,安全管理器若不希望執行該操作,會拋一個異常給Java API,否則Java API將完成操作並正常返回。

1.初識SecurityManager

SecurityManager類是Java API中一個相當關鍵的類,它為其他Java API提供相應的介面,使之可以檢查某項操作能否執行,充當了安全管理器的角色。我們從下面的程式碼來看安全管理器是如何工作的?

    public static void main(String[] args) {
        String s;
        try {
            FileReader fr = new FileReader(new File("E:\\test.txt"));
            BufferedReader br = new BufferedReader(fr);
            while ((s = br.readLine()) != null)
                System.out.println(s);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
複製程式碼

第一步,在建立FileReader物件的時候會先根據File物件建立FileInputStream例項,原始碼如下:

    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();
        }
        if (file.isInvalid()) {
            throw new FileNotFoundException("Invalid file path");
        }
        fd = new FileDescriptor();
        fd.attach(this);
        path = name;
        open(name);
    }
複製程式碼

第二步,Java API希望建立一個讀取File的位元組流物件,首先必須獲取當前系統的安全管理器,然後通過安全管理器進行操作校驗,若通過,再呼叫私有方法真正執行操作(open()是FileInputStream類的私有例項方法),若校驗失敗,則丟擲一個安全異常,層層上拋,直至使用者面前。

    public void checkRead(String file) {
        checkPermission(new FilePermission(file,SecurityConstants.FILE_READ_ACTION));
    }
    
    public void checkPermission(Permission perm) {
        java.security.AccessController.checkPermission(perm);
    }
複製程式碼

上面便是此處涉及SecurityManager的兩個方法原始碼(jdk1.8)。可以看到,SecurityManager對訪問檔案的校驗,最終是交由存取控制器實現的,AccessController在檢查許可權期間則會丟擲一個AccessControlException異常告訴呼叫者校驗失敗。該異常繼承自SecurityException,SecurityException又繼承自RuntimeException,因此AccessControlException是一個執行期異常。通常,呼叫方法往往涉及到一系列其他方法的呼叫,一旦出現安全異常,異常會順著呼叫鏈傳向頂部方法,最後執行緒中斷結束。

2.操作SecurityManager

一般情況下,安全管理器是預設沒有被安裝的。因此,上面建立FileInputStream的原始碼中,security==null,是不會執行checkRead的(感興趣的同學可以在main方法裡直接使用System提供的方法進行驗證)。System類為使用者操作安全管理器提供了兩個方法。

public static SecurityManager getSecurityManager() 該方法用於獲得當前安裝的安全管理器引用,若未安裝,返回null。 public static void setSecurityManager(final SecurityManager s) 該方法用於將指定的安全管理器的例項設定為系統的安全管理器。

上面讀取test.txt的程式碼時可以正常執行的,控制檯會一行一行列印檔案的內容。在配置上自定義的安全管理器(繼承SecurityManager,重寫checkRead方法)後,再看執行結果。

public class Main {

    class SecurityManagerImpl extends SecurityManager {

        public void checkRead(String file) {
            throw new SecurityException();
        }
    }
    
    public static void main(String[] args) {
        System.out.println("CurrentSecurityManager is " + System.getSecurityManager());
        Main m = new Main();
        System.setSecurityManager(m.new SecurityManagerImpl());
        System.out.println("CurrentSecurityManager is " + System.getSecurityManager());
        String s;
        try {
            FileReader fr = new FileReader(new File("E:\\test.txt"));
            BufferedReader br = new BufferedReader(fr);
            while ((s = br.readLine()) != null) {
                System.out.println(s);
            }
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

執行結果:

CurrentSecurityManager is null
CurrentSecurityManager is Main$SecurityManagerImpl@135fbaa4
Exception in thread "main" java.lang.SecurityException
	at Main$SecurityManagerImpl.checkRead(Main.java:10)
	at java.io.FileInputStream.<init>(FileInputStream.java:127)
	at java.io.FileReader.<init>(FileReader.java:72)
	at Main.main(Main.java:21)
複製程式碼

注:如果想要java環境安裝預設的管理器,一種方式如上設定預設安全管理器的例項,另一種方式也可以在配置JVM 執行引數的時候加上-Djava.security.manager。一般推薦後者,因為可以不用去改動程式碼,同時可以靈活的通過再配置一個-Djava.security.policy="x:/xxx.policy"引數的方式指定安全策略檔案。

3.使用SecurityManager

安全管理器提供了各個方面的安全檢查的公共方法,允許任意呼叫。核心Java API中有很多方法,直接或間接呼叫安全管理器提供的方法實現各自的安全檢查操作。在安全檢查中還存在一個概念,可信類與不可信類。顯然,一個類不是可信類就是不可信類。

如果一個類是核心Java API類,或者它顯示地擁有執行某項操作的許可權,那麼這個類就是可信類。

3.1 檔案訪問相關的安全檢查方法

這裡的檔案訪問指的是區域網中檔案訪問的處理,並不單單是本地磁碟上的檔案訪問。

public void checkRead(FileDescriptor fd)

public void checkRead(String file)

public void checkRead(String file, Object context)

檢查程式能否讀取指定檔案。不同入參代表不同的校驗方式。第一個方法校驗當前保護域是否擁有名為readFileDescriptor的執行時許可權,第二個方法檢驗當前保護域是否擁有指定檔案的讀許可權,第三個方法和第二個方法相同,不同的是在指定的存取控制器上下文中檢驗。

public void checkWrite(FileDescriptor fd)

public void checkWrite(String file)

檢查是否允許程式寫指定檔案。第一個方法校驗當前保護域是否擁有名為writeFileDescriptor的執行時許可權,第二個方法檢驗當前保護域是否擁有指定檔案的寫許可權。

public void checkDelete(String file)

檢查是否允許程式刪除指定檔案。檢驗當前保護域是否擁有指定檔案的刪除許可權。

下表簡單列出了Java API中直接呼叫了checkRead()、checkWrite()和checkDelete()的方法。

Java安全:SecurityManager與AccessController

3.2 網路訪問相關的安全檢查方法

Java中的網路訪問一般是通過開啟一個網路套接字實現的。網路套接字在邏輯上分為客戶套接字和伺服器套接字兩類。

public void checkConnect(String host, int port)

public void checkConnect(String host, int port, Object context)

檢查程式能否向指定的主機上指定的埠開啟一個客戶套接字。檢驗當前保護域是否擁有指定主機名和埠的連線許可權。

public void checkListen(int port)

檢查程式能否建立一個監聽特定埠的伺服器套接字。

public void checkAccept(String host, int port)

檢查程式能否在當前伺服器套接字上接收指定主機和埠發出的客戶連線。

public void checkMulticast(InetAddress maddr)

檢查程式能否在指定的多播地址上建立一個多播套接字。

public void checkSetFactory()

檢查程式能否修改預設的套接字實現。使用Socket建立套接字時,會由套接字工廠獲得一個新的套接字。程式可以通過安裝套接字工廠擴充套件不同語義的套接字,這就要求保護域擁有名為setFactory的執行時許可權。

3.3 保護Java虛擬機器的安全檢查方法

對於不可信類,有必要去提供一些方法避免它們繞過安全管理器和Java API,從而保證Java虛擬機器的安全。

public void checkCreateClassLoader()

檢查當前保護域是否擁有creatClassLoader的執行時許可權,確定程式能否建立一個類載入器。

public void checkExec(String cmd)

檢查保護域是否擁有指定命令的執行許可權,確定程式能否執行一個系統命令。

public void checkLink(String lib)

檢查程式能否程式能否鏈入虛擬機器中連結共享庫(原生程式碼通過該庫執行)。

public void checkExit(int status)

檢查程式是否有許可權關閉虛擬機器。

public void checkPermission(Permission perm) public void checkPermission(Permission perm, Object context)

檢查當前保護域(可以理解為當前執行緒)是否擁有指定的許可權。

3.4 保護程式執行緒的安全檢查方法

一個Java程式的執行依賴於很多執行緒。除了程式本身的執行緒,虛擬機器會自動為使用者建立很多系統級的執行緒,比如垃圾回收、管理相關介面的輸入輸出請求等等。不可信類是不能管理這些影響程式的執行緒的。

public void checkAccess(Thread t)

public void checkAccess(ThreadGroup g)

檢查是否允許修改指定執行緒(執行緒組及組內執行緒)的狀態。

3.5 保護系統資源的安全檢查方法

Java程式是可以訪問一些系統級的資源的,比如列印任務、剪貼簿、系統屬性等等。出於安全考慮,不可信類是不能訪問這些資源的。

public void checkPrintJobAccess()

檢查程式能否訪問使用者印表機(queuePrintJob-執行時許可權)

public void checkSystemClipboardAccess()

檢查程式是否可以訪問系統剪貼簿(accessClipboard-AWT許可權)

public void checkAwtEventQueueAccess()

檢查程式能否獲得系統時間佇列(accessEventQueue-AWT許可權)

public void checkPropertiesAccess() public void checkPropertyAccess(String key)

檢查程式嫩否獲取Java虛擬機器擁有的系統屬性資訊

public boolean checkTopLevelWindow(Object window)

檢查程式能否在桌面新建一個視窗

3.6 保護Java 安全機制本身的安全檢查方法

public void checkMemberAccess(Class<?> clazz, int which)

反射時檢查程式能否訪問類的成員。

public void checkSecurityAccess(String target)

檢查程式能否執行安全有關的操作。

public void checkPackageAccess(String pkg)

public void checkPackageDefinition(String pkg)

在使用類裝載器裝載某個類且指定了包名時,會檢查程式能否訪問指定包下的內容。

三、存取控制器

核心Java API由安全管理器提供安全策略,但是大多數安全管理器的實現是基於存取控制器的。

1. 建立存取控制器的基礎

程式碼源:對於從其上裝載Java類的地址,需要用程式碼源進行封裝

許可權:要實現某個特定操作,需要許可權封裝相應的請求

策略:對指定程式碼源授予相應的許可權,策略可以表示為對所有許可權的封裝

保護域:對程式碼源及該程式碼源相應許可權的封裝

1.1 CodeSource

程式碼源物件表示從其上裝在類的URL地址,以及類的簽名相關資訊,由類裝載器負責建立和管理。

public CodeSource(URL url, Certificate certs[])

構造器函式,針對指定url裝載得到的程式碼,建立一個程式碼源物件。第二個引數是證照陣列,可選,用來指定公開金鑰,該金鑰可以實現對程式碼的簽名。

public boolean implies(CodeSource codesource)

按照許可權類的(Permission)的要求,判斷當前程式碼源能否用來表示引數所指定的程式碼源。一個程式碼源能表示另一個程式碼源的條件是,前者必須包括後者的所有證照,而且由前者的URL可以獲得後者地URL。

1.2 Permission

Permission類的例項物件就是許可權物件,它是存取控制器處理的基本實體。Permission類是一個抽象類,不同的實現類在安全策略檔案中體現為不同的許可權型別。Permission類的一個例項代表一個特定的許可權,一組特定的許可權則由Permissions的一個例項表示。

要實現自定義許可權類的時候需要繼承Permission類的,其抽象方法如下:

//校驗許可權引數物件擁有的許可權名和許可權操作是否符合建立物件時的設定是否一致
public abstract boolean implies(Permission permission);
//比較兩個許可權物件的型別、許可權名以及許可權操作
public abstract boolean equals(Object obj);
public abstract int hashCode();
//返回建立物件時設定的許可權操作,未設定返回空字串
public abstract String getActions();
複製程式碼

1.3 Policy

存取控制器需要確定許可權應用於哪些程式碼源,從而為其提供相應的功能,這就是所謂的安全策略。Java使用了Policy對安全策略進行了封裝,預設的安全策略類由sun.security.provider.PolicyFile提供,該類基於jdk中配置的策略檔案(%JAVA_HOME%/ jre/lib/security/java.policy)進行對特定程式碼源的許可權配置。預設配置如下:

// Standard extensions get all permissions by default

grant codeBase "file:${{java.ext.dirs}}/*" {
        permission java.security.AllPermission;
};

// default permissions granted to all domains

grant {
        // Allows any thread to stop itself using the java.lang.Thread.stop()
        // method that takes no argument.
        // Note that this permission is granted by default only to remain
        // backwards compatible.
        // It is strongly recommended that you either remove this permission
        // from this policy file or further restrict it to code sources
        // that you specify, because Thread.stop() is potentially unsafe.
        // See the API specification of java.lang.Thread.stop() for more
        // information.
        permission java.lang.RuntimePermission "stopThread";
        // allows anyone to listen on dynamic ports
        permission java.net.SocketPermission "localhost:0", "listen";

        // "standard" properies that can be read by anyone

        permission java.util.PropertyPermission "java.version", "read";
        permission java.util.PropertyPermission "java.vendor", "read";
        permission java.util.PropertyPermission "java.vendor.url", "read";
        permission java.util.PropertyPermission "java.class.version", "read";
        permission java.util.PropertyPermission "os.name", "read";
        permission java.util.PropertyPermission "os.version", "read";
        permission java.util.PropertyPermission "os.arch", "read";
        permission java.util.PropertyPermission "file.separator", "read";
        permission java.util.PropertyPermission "path.separator", "read";
        permission java.util.PropertyPermission "line.separator", "read";

        permission java.util.PropertyPermission "java.specification.version", "read";
        permission java.util.PropertyPermission "java.specification.vendor", "read";
        permission java.util.PropertyPermission "java.specification.name", "read";

        permission java.util.PropertyPermission "java.vm.specification.version", "read";
        permission java.util.PropertyPermission "java.vm.specification.vendor", "read";
        permission java.util.PropertyPermission "java.vm.specification.name", "read";
        permission java.util.PropertyPermission "java.vm.version", "read";
        permission java.util.PropertyPermission "java.vm.vendor", "read";
        permission java.util.PropertyPermission "java.vm.name", "read";
};
複製程式碼

第一個grant定義了系統屬性${{java.ext.dirs}}路徑下的所有的class及jar(/*號表示所有class和jar,如果只是/則表示所有class但不包括jar)擁有所有的操作許可權(java.security.AllPermission),java.ext.dirs對應路徑為%JAVA_HOME%/jre/lib/ext目錄,而第二個grant後面定義了所有JAVA程式都擁有的許可權,包括停止執行緒、啟動Socket 伺服器、讀取部分系統屬性。

Policy類提供addStaticPerms(PermissionCollection perms, PermissionCollection statics)方法新增特定許可權集給策略物件內部的許可權集,也提供public PermissionCollection getPermissions(CodeSource codesource)方法設定安全策略的許可權集給來自特定程式碼源的類。

虛擬機器中任何情況下只能安裝一個安全策略類的例項,但是可以通過Policy.setPolicy(Policy p)替換當前系統的安全策略,也可以通過Policy.getPolicy()獲得程式當前的安全策略類。

1.4 ProtectionDomain

保護域就是一個授權項,可以理解為是程式碼源和對應許可權的組合。虛擬機器中每個類都屬於且僅屬於一個保護域,由程式碼源指定的地址裝載得到,同時程式碼源所在保護域包含的許可權集規定了一些許可權,這個類就擁有這些許可權。保護域的構造方法如下:

public ProtectionDomain(CodeSource codesource,PermissionCollection permissions)

2. 存取控制器AccessController

AccessController類的構造器是私有的,因此不能對其進行例項化。它向外部提供了一些靜態方法,其中最關鍵的就是checkPermission(Permission p),該方法基於當前安裝的Policy物件,判定當前保護欲是否擁有指定許可權。安全管理器SecurityManager提供的一系列check***的方法,最後基本都是通過AccessController.checkPermission(Permission p)完成。

public static void main(String[] args) {
        System.setSecurityManager(new SecurityManager());
        SocketPermission sp = new SocketPermission(
                "127.0.0.1:6000", "connect");
        try {
            AccessController.checkPermission(sp);
            System.out.println("Ok to open socket");
        } catch (AccessControlException ace) {
            System.out.println(ace);
        }
    }
複製程式碼

上面的程式碼首先安裝了預設的安全管理器,然後例項化了一個連線本地6000埠的許可權物件,最後通過存取控制器檢查。 列印結果如下:

java.security.AccessControlException: access denied ("java.net.SocketPermission" "127.0.0.1:6000" "connect,resolve")
複製程式碼

存取控制器丟擲了一個異常,提示沒有連線該地址的許可權。在預設的安全策略檔案上配置此埠的連線許可權:

permission java.net.SocketPermission "127.0.0.1:6000", "connect";

列印結果:

Ok to open socket

實際工作中,可能會面臨多個專案之間的方法呼叫。假設有兩個專案A和B,A專案中的TestA類中有testA()方法內部呼叫了B專案中的TestB類的testB()方法,去開啟一個專案B所在伺服器的套接字。在許可權校驗時,要想此種呼叫正常操作。需要在A專案所在虛擬機器的安全策略檔案中配置TestA類開啟專案B所在伺服器制定埠的連線許可權,同時還要在B專案所在虛擬機器的安全策略檔案中配置TestB類開啟同一地址及埠的連線許可權。這種操作方式固然可以,但是顯然太複雜且不可預計。AccessController提供了doPivileged()方法,為呼叫者臨時開放許可權,但是要求被呼叫者必須有對應操作的許可權。

相關文章