保護計算機上的信息不被非法獲取和修改時Java最初的,也是最基本的設計目標,但同時還要保證Java程序在主機上的運行不受影響。java
JDK自己提供了基本的安全方面的功能,好比可配置的安全策略、生成消息摘要、生成數字簽名等等。同時,Java也有一些擴展程序,更加全面地支撐了整個安全體系。web
Java加密擴展包(JCE)提供了密碼、安全密鑰交換、安全消息摘要、密鑰管理系統等功能。數組
Java安全套接字擴展包(JSSE)提供了SSL(安全套接字層)的加密功能,保證了與SSL服務器或SSL客戶的通訊安全。安全
Java鑑別與受權服務(JAAS)能夠在Java平臺上提供用戶鑑別,而且容許開發者根據用戶提供的鑑別信任狀准許或拒絕用戶對程序的訪問。bash
如何理解?程序要在主機上安裝,那麼主機必須爲該程序提供一個運行的場所(運行環境),該場所支持程序運行的同時,也限制其能夠獲取的資源。就比如小孩子去你家玩,你須要提供一個空間讓她玩耍且不會受傷,同時還要保證你女友新買的化妝鏡不會被孩子打碎。服務器
Java沙箱負責保護一些系統資源,並且保護級別是不一樣的。網絡
通常來說,沙箱的默認狀態容許其中的程序訪問CPU、內存等資源,以及其上裝在的Web服務器。若沙箱徹底開放,則其中程序的權限與主機相同。多線程
當前最新的安全機制實現,則引入了域 (Domain) 的概念,能夠理解爲將沙箱細分爲多個具體的小沙箱。虛擬機會把全部代碼加載到不一樣的系統域和應用域,系統域部分專門負責與關鍵資源進行交互,而各個應用域部分則經過系統域的部分代理來對各類須要的資源進行訪問。虛擬機中不一樣的受保護域 (Protected Domain),對應不同的權限 (Permission)。存在於不一樣域中的類文件就具備了當前域的所有權限,以下圖所示: app
沙箱的實現取決於下面三方面的內容:dom
從Java API的角度去看,應用程序的安全策略是由安全管理器去管理的。安全管理器決定應用是否能夠執行某項操做。這些操做具體是否能夠執行的依據,是看其可否對一些比較重要的系統資源進行訪問,而這項驗證由存取控制器進行管控。這麼看來,存取控制器是安全管理器的基礎實現,安全管理器能作的,存取控制器也能夠作。那麼問題來了,爲何還須要安全管理器?
Java2之前是沒有存取控制器的,那個時候安全管理器利用其內部邏輯決定應用的安全策略,若要調整安全策略,必須修改安全管理器自己。Java2開始,安全管理器將這些工做交由存取控制器,存取控制器能夠利用策略文件靈活地指定安全策略,同時還提供了一個更簡單的方法,實現了更細粒度地將特定權限授予特定的類。所以,Java2以前的程序都是利用安全管理器的接口實現系統安全的,這意味着安全管理器是不能修改的,那麼引入的存取控制器並不能徹底替代安全管理器。二者的關係以下圖:
安全管理器是Java API和應用程序之間的「第三方權威機構」。比如貸款時,銀行會根據央行徵信系統查詢用戶的信用狀況決定是否放款。Java應用程序請求Java API完成某個操做,Java API會向安全管理器詢問是否能夠執行,安全管理器若不但願執行該操做,會拋一個異常給Java API,不然Java API將完成操做並正常返回。
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是一個運行期異常。一般,調用方法每每涉及到一系列其餘方法的調用,一旦出現安全異常,異常會順着調用鏈傳向頂部方法,最後線程中斷結束。
通常狀況下,安全管理器是默認沒有被安裝的。所以,上面建立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"參數的方式指定安全策略文件。
安全管理器提供了各個方面的安全檢查的公共方法,容許任意調用。核心Java API中有不少方法,直接或間接調用安全管理器提供的方法實現各自的安全檢查操做。在安全檢查中還存在一個概念,可信類與不可信類。顯然,一個類不是可信類就是不可信類。
若是一個類是核心Java API類,或者它顯示地擁有執行某項操做的權限,那麼這個類就是可信類。
這裏的文件訪問指的是局域網中文件訪問的處理,並不僅僅是本地磁盤上的文件訪問。
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中的網絡訪問通常是經過打開一個網絡套接字實現的。網絡套接字在邏輯上分爲客戶套接字和服務器套接字兩類。
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的運行時權限。
對於不可信類,有必要去提供一些方法避免它們繞過安全管理器和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)
檢查當前保護域(能夠理解爲當前線程)是否擁有指定的權限。
一個Java程序的運行依賴於不少線程。除了程序自己的線程,虛擬機會自動爲用戶建立不少系統級的線程,好比垃圾回收、管理相關接口的輸入輸出請求等等。不可信類是不能管理這些影響程序的線程的。
public void checkAccess(Thread t)
public void checkAccess(ThreadGroup g)
檢查是否容許修改指定線程(線程組及組內線程)的狀態。
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)
檢查程序可否在桌面新建一個窗口
public void checkMemberAccess(Class<?> clazz, int which)
反射時檢查程序可否訪問類的成員。
public void checkSecurityAccess(String target)
檢查程序可否執行安全有關的操做。
public void checkPackageAccess(String pkg)
public void checkPackageDefinition(String pkg)
在使用類裝載器裝載某個類且指定了包名時,會檢查程序可否訪問指定包下的內容。
核心Java API由安全管理器提供安全策略,可是大多數安全管理器的實現是基於存取控制器的。
代碼源:對於從其上裝載Java類的地址,須要用代碼源進行封裝
權限:要實現某個特定操做,須要權限封裝相應的請求
策略:對指定代碼源授予相應的權限,策略能夠表示爲對全部權限的封裝
保護域:對代碼源及該代碼源相應權限的封裝
代碼源對象表示從其上裝在類的URL地址,以及類的簽名相關信息,由類裝載器負責建立和管理。
public CodeSource(URL url, Certificate certs[])
構造器函數,針對指定url裝載獲得的代碼,建立一個代碼源對象。第二個參數是證書數組,可選,用來指定公開密鑰,該密鑰能夠實現對代碼的簽名。
public boolean implies(CodeSource codesource)
按照權限類的(Permission)的要求,判斷當前代碼源可否用來表示參數所指定的代碼源。一個代碼源能表示另外一個代碼源的條件是,前者必須包括後者的全部證書,並且由前者的URL能夠得到後者地URL。
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();
複製代碼
存取控制器須要肯定權限應用於哪些代碼源,從而爲其提供相應的功能,這就是所謂的安全策略。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()
得到程序當前的安全策略類。
保護域就是一個受權項,能夠理解爲是代碼源和對應權限的組合。虛擬機中每一個類都屬於且僅屬於一個保護域,由代碼源指定的地址裝載獲得,同時代碼源所在保護域包含的權限集規定了一些權限,這個類就擁有這些權限。保護域的構造方法以下:
public ProtectionDomain(CodeSource codesource,PermissionCollection permissions)
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()方法,爲調用者臨時開放權限,可是要求被調用者必須有對應操做的權限。