Java安全:SecurityManager與AccessController

原文地址:
Java安全:SecurityManager與AccessController


前言


什麼是安全?

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

保護計算機上的信息不被非法獲取和修改時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 API確定與安全相關的操作是否允許執行。
  • 存取控制器,安全管理器默認實現的基礎。
  • 類裝載器,可以實現安全策略和類的封裝。

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

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

在這裏插入圖片描述

二、 安全管理器


安全管理器是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類爲用戶操作安全管理器提供了兩個方法。

//該方法用於獲得當前安裝的安全管理器引用,若未安裝,返回null。
public static SecurityManager getSecurityManager()  
//該方法用於將指定的安全管理器的實例設置爲系統的安全管理器。
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()的方法。

在這裏插入圖片描述

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()方法,爲調用者臨時開放權限,但是要求被調用者必須有對應操作的權限。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章