Java的class只在需要的時候才內轉載入內存,并由java虛擬機的執行引擎來執行,而執行引擎從總的來說主要的執行方式分為四種,
第一種,一次性解釋代碼,也就是當字節碼轉載到內存后,每次需要都會重新的解析一次,
第二種,即時解析,也就是轉載到內存的字節碼會被解析成本地機器碼,并緩存起來以提高重用性,但是比較耗內存,
第三種,自適應優化解析,即將java將使用最頻繁的代碼編譯成本地機器碼,而使用不頻繁的則保持字節碼不變,一個自適應的優化器可以使得java虛擬機在80%-90%的時間里執行優化過的本地代碼,而只需要執行10%-20%對性能有影響的代碼。
第四種,一種能夠利用本地方法直接解析java字節碼的芯片。Java TCP網絡基礎
在了解Java虛擬機的類裝載器之前,有一個概念我們是必須先知道的,就是java的沙箱,什么是java的沙箱,java的沙箱總體上經歷了這么一個過程,從簡單的java1.0的基礎沙箱到java1.1的基于簽名和認證的沙箱到后來基于基礎沙箱+簽名認證沙箱的java1.2的細粒度訪問控制。
java的沙箱是你可以接受來自任何來源的代碼,但沙箱限制了它進行可能破壞系統的任何動作,因為沙箱相對于系統的總的訪問能力已經被限制,所以沙箱形象的說更像是一個監獄,把有破壞能力的代碼困住了。
java沙箱的基本組件如下:
1.類裝載器結構(可以由用戶定制)
2.class文件檢驗器
3.內置的java虛擬機
4.安全管理器(可以由用戶定制)
5.java核心API
java的沙箱中類裝載器和安全管理器可以由用戶定制的,但是這樣就加大了java代碼安全的風險,所以java有一個叫訪問控制體系結構,他包括安全策略規范和運行時安全策略實施,java有一個默認的安全策略管理器,用戶可以使用默認的安全策略管理器也可以在它之上進行擴展。
java的類裝載器從三方面對java的沙箱起作用:
怎么理解這句話,不同的類裝載器裝入同樣的類的時候會產生一個唯一的命名空間,java虛擬機維護著這些命名空間,同一類,一個命名空間只能裝載一次,也只會裝載一次,不同命名空間之間的類就如同各自有一個防護罩,感覺不到彼此的存在,如下圖3-1所示
這里有兩個需要理解的概念,一,雙親委托模式,二運行時包,java虛擬機通過這兩個方面來界定類庫的邊界
什么是雙親委托模式
先來看一個圖和一段代碼
這個圖說明了類裝載的過程,但是光這么看還是沒有那么的清晰,我們只知道虛擬機啟動的時候會啟動bootStrapClassLoader,它負責加載java的核心API,然后bootStrapClassLoader會裝載
Launcher.java 之中的ExtClassLoader(擴展類裝載器),并設定其Parent 為 null ,代表其父加載器為BootstrapLoaderExtClassLoader再有ExtClassLoader去裝載ext下的拓展類庫,然后 Bootstrap Loader 再要求加載 Launcher.java 之中的 AppClassLoader(用戶自定義類裝載器) ,并設定其 Parent 為之前產生的 ExtClassLoader 實體。這兩個加載器都是以靜態類的形式存在的,下面我們找到java.lang.ClassLoader的loadClass這個方法
PRotected synchronized Class<?>loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//First, check if the class has already been loaded
Class c = findLoadedClass(name);
if(c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if(resolve) {
resolveClass(c);
}
return c;
}
這個方法告訴我們雙親委托模式的過程,當虛擬機去裝載一個類的時候會先調用一個叫loadClass的方法,接著在這個方法里它會先調用findLoadedClass來判斷要裝載的類字節碼是否已經轉入了內存,如果沒有的話,它會找到它的parent(這里的parent指裝載自己的那個類加載器,一般我們的應用程序類的parent是AppClassLoader),然后調用parent的loadClass,重復自己loadClass的過程,如果parent沒有裝載過著這個類,就調用findBootstrapClass(這里是指bootStrap,啟動裝載器)來嘗試裝載這個類的字節碼,如果bootStrap也沒有辦法裝載這個類,則調用自己的findClass來嘗試裝載這個類,如果還是沒辦法裝載則拋出異常。
上面就是對雙親模式的簡單描述,那么雙親委托描述有什么好處?
你嘗試一下自己寫個java.lang.String的類,然后在ecplise跑一下,有沒有發現拋出了異常,來看看這個異常
java.lang.NoSuchMethodError: main
運行這個我們自己定義的類的java.lang.String的雙親委托模式加載過程如下AppClassLoader -> ExtClassLoader -> BootstrapLoader,由于BootstrapLoader只會加載核心API里的類,它匹配到核心API(JAVA_HOME/jre/lib)里的String類,所以它以為找到了這個類就直接去尋找核心API里的String類里的main函數,所以就拋出異常了,而我們自己寫的那個String根本就沒有機會被加載入內存,這就防止了我們自己寫的類對java核心代碼的破壞。
什么是運行時包
要了解運行時包,我們先來設想一個問題,如果你自己定義了一個java.lang.A的類,能不能訪問到java.lang.String類的friend成員?
不行,為什么?這就是運行時包在起作用,java的語法規定,包訪問權限的成員能夠被同一個包下的類訪問,那是為什么不能夠訪問呢,這同樣是為了防止病毒代碼的破壞,java虛擬機只允許由同一個類裝載器裝載到同一包中的類型互相訪問,而由同一類裝載器裝載,屬于同一個包的,多個類型的集合就是我們所指的運行時包了。
除了1.屏蔽不同的命名空間,2.保護信任類庫的邊界外,類裝載器的第三個重要的作用就是保護域,類裝載器必須把代碼放入到保護域中以限定這些代碼運行時能夠執行的操作的權限,這也如我上面講的,像一個監獄一樣,不讓它在監獄意外的范圍活動。
前面的學習我們知道了class文件被類裝載器所裝載,但是在裝載class文件之前或之后,class文件實際上還需要被校驗,這就是今天的學習主題,class文件校驗器。
class文件校驗器,保證class文件內容有正確的內部結構,Java虛擬機的class文件檢驗器在字節碼執行之前對文件進行校驗,而不是在執行中進行校驗
class文件校驗器要進行四趟獨立的掃描來完成校驗工作
在裝載字節序列的時候進行,這個是校驗class文件的結構的合法性,比如你使用windowns下的copy命令去合并一個.class文件和一個jpg文件的時候,在裝載這個class文件的時候jvm會發現這個class文件被刪改過,文件的長度也不正確,而拋出異常!
所以這次校驗是發生在二進制數據上,
掃描發生在方法區中,主要對于,語義,詞法和語法的分析,也就是檢查這個類是否能夠順利的編譯!
字節碼校驗
在這一趟的校驗中涉及兩個比較不好理解的概念,第一個是字節碼流,第二個是棧幀.
執行字節碼時,一次執行操作碼,java虛擬機內構成了執行線程,而每個線程會有自己的java棧就是我們說的棧幀。每一個方法都有一個棧幀。
如果學過匯編的人理解這兩個概念會容易一點
字節碼流=操作碼+操作數,在這里可以看做匯編里的偽指令+操作數,因為這里的操作碼實際上就是給jvm識別的“匯編偽指令”,而操作數的概念和匯編里的除了數據類型,并沒有多大的差異
重點來看一下棧幀,棧幀其實也很好理解,棧幀里有局部變量棧和操作數棧,這兩塊內存就是放數據的時機不同,操作數棧就是用來存放字節碼指令執行的中間結果,結果或操作數,而局部變量區,就是用來存局部變量形參等,這個很好理解
這個字節碼的校驗過程校驗的就是字節碼流的合法過程,也就是校驗操作數+操作碼的合法性。
而java的class文件編碼我們之所以稱之為字節碼,是因為每調條操作指令都只占一個字節,除了兩個例外情況,所有的操作碼和他們的操作數按字節對齊,這使得字節流在傳輸的時候跟小,更有優勢,這兩個例外是這樣一些操作碼,在操作碼和他們的操作數之間會天上一至三個字節,以便操作數都按字節對齊。
下面是一個圖,描述了棧幀的結構
符號引用的校驗
由于大部分jvm的實現都是延遲加載或者說動態鏈接的,延遲加載的意思就是,jvm裝載某個類A時,如果A類里有引用其他的類B,虛擬機并不會把這個被引用B類也同時裝載入內存,而是等到執行到的時候才去裝載。
而這個被引用的B類在引用它的類A中的表現形式主要被登記在了符號表中,而第四趟的這個過程就是當需要用到被引用類B的時候,將被引用類B在引用類A的符號引用名改為內存里的直接引用
所以第四趟發生的時間是不可預料的,而且發生在方法區中。總個這個過程稱之為動態連接
可以簡單的劃分為兩步
1.查找被引用的類(有必要的話就加載它)
2.將符號引用替換為直接引用,例如一個指向類、字段或方法的指針,下次再需要用到被引用類的時候直接運用直接引用,不需要再去裝載。
這個過程其實在ClassLoader類中的loadClass中就可以發現它的痕跡。我們先貼出loadClass這個方法實現,然后簡要的做一下分析
protected synchronized Class<?>loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
loadClass有兩個參數,第一個參數是類的全限定名,第二個參數就是我們要說的重點,這個參數為true的時候表示,loadClass方法會執行resolveClass的方法,這個方法就是將類中的符號引用替換為直接引用。最終調用的方法是一個本地方法 resolveClass0。
這里還有一點需要注意,Class.forName這個靜態的方法我們也常用來加載class文件的字節碼,那它和classLoader有什么區別?
區別就在于是否執行resolveClass這個方法,Class.forName總是承諾將符號連接進行連接和初始化,而loadClass沒有這樣的承諾。
總結:
第一趟掃描,在類被裝載時進行,校驗class文件的內部結構,保證能夠被正常安全的編譯
第二趟和第三趟在連接的過程中進行,這兩趟基本上是語法校驗,詞法校驗
第四趟是解析符號引用和直接引用時進行的,這次校驗確認被引用的類,字段以及方法確實存在
前面已經簡述了Java的安全模型的兩個組成部分(類裝載器,class文件校驗器),接下來學習的是java安全模型的另外一個重要組成部分安全管理器。
安全管理器是一個單獨的對象,在java虛擬機中,它在訪問控制-對于外部資源的訪問控制-起到中樞作用
如果光看概念可能并不能很好的理解,或者說比較抽象,下面是ClassLoader其中的一個構造函數,先簡單的看看它在初始化ClassLoader之前會做一些什么操作
protected ClassLoader(ClassLoader parent){
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
this.parent = parent;
initialized = true;
}
這個構造函數的第一話(當然還有隱式調用)就是System.getSecurityManager();這行代碼返回的就是一個安全管理器對象security,這個對象所屬的目錄為java.lang.SecurityManager。
這個構造函數先判斷如果已經安裝了安全管理器security(在前面類裝載器的章節,我們提到過,類裝載器和安全管理器是可以由用戶定制的,在這里有了體現吧!!既然有System.getSecurityManager();你當然也應該猜到有System.setSecurityManager();),也就是安全管理器不為空,那么就執行校驗,跳到checkCreateClassLoader();看看他做的是什么操作
public void checkCreateClassLoader() {
checkPermission(SecurityConstants.CREATE_CLASSLOADER_PERMISSION);
}
這里又調用了另外一個方法,從方法名字上,就可以猜到這個方法是用來校驗權限的,校驗是否有創建ClassLoader的權限,再跳到checkPermisson方法里
public static voidcheckPermission(Permission perm) throws accessControlException {
//System.err.println("checkPermission "+perm);
//Thread.currentThread().dumpStack();
if(perm == null) {
thrownew NullPointerException("permission can't be null");
}
AccessControlContextstack = getStackAccessControlContext();
//if context is null, we had privileged system code on the stack.
if(stack == null) {
Debugdebug = AccessControlContext.getDebug();
booleandumpDebug = false;
if(debug != null) {
dumpDebug= !Debug.isOn("codebase=");
dumpDebug&= !Debug.isOn("permission=")
||Debug.isOn("permission=" + perm.getClass().getCanonicalName());
}
if(dumpDebug && Debug.isOn("stack")) {
Thread.currentThread().dumpStack();
}
if(dumpDebug && Debug.isOn("domain")) {
debug.println("domain(context is null)");
}
if(dumpDebug) {
debug.println("accessallowed " + perm);
}
return;
}
AccessControlContextacc = stack.optimize();
acc.checkPermission(perm);
}
上面的這個方法有些代碼比較難以理解,我們不用每行都讀懂(這個方法涉及的東西比較多,它涉及到了代碼簽名認證,策略還有保護域,這些我們在后一節中會詳細的講解,看不懂先跳過),看它的注解// if context is null, we hadprivileged system code on the stack.意思就是如果當前的訪問控制器上下文為空,在棧上的系統代碼將得到特權,找到acc.checkPermission(perm);再跳進去找到下面這段代碼
/*
*iterate through the ProtectionDomains in the context.
*Stop at the first one that doesn't allow the
*requested permission (throwing an exception).
*
*/
/* if ctxt is null, all we had on the stackwere system domains,
or the first domain was a Privileged system domain. This
is to make the common case for system code very fast */
if (context == null)
return;
for (int i = 0; i < context.length; i++){
if(context[i] != null && !context[i].implies(perm)) {
if(dumpDebug) {
debug.println("accessdenied " + perm);
}
if(Debug.isOn("failure") && debug != null) {
//Want to make sure this is always displayed for failure,
//but do not want to display again if already displayed
//above.
if(!dumpDebug) {
debug.println("accessdenied " + perm);
}
Thread.currentThread().dumpStack();
finalProtectionDomain pd = context[i];
finalDebug db = debug;
AccessController.doPrivileged(newPrivilegedAction() {
publicObject run() {
db.println("domainthat failed " + pd);
returnnull;
}
});
}
thrownew AccessControlException("access denied " + perm, perm);
}
}
什么都不用看,就看最上面的那段注解,意思是遍歷上下文中的保護域,一旦發現請求的權限不被允許,停止,拋出異常,到這里我們有一個比較清晰的概念了,安全管理器就是用來控制執行權限的,而上面的這段代碼中有一個很重要的類 AccessController,訪問控制器,還有一個很重要的名詞保護域(保護域我們在前面一節也有簡單的帶過一下,是不是有點印象),這些可能現在聽有點模糊,不要擔心,暫時不要管,后面一章節慢慢的會對他們進行講解。
好了了解安全管理器是做什么的之后,接下來,來做一個下的實驗,先來驗證,默認安全管理是沒有被安裝的,接著來試著把他安裝上去。在我的環境中我是沒有安裝默認的安全管理器的,也沒有基于默認的安全管理器寫自己的安全管理器,如果需要打開的話,可以在程序顯示的安裝安全管理器,同樣可以讓它自動安裝默認的安全管理器(給jvm加上-Djava.security.manager就可以了。
下面我們用熟悉的ecplise寫一個簡單的demo來看看安裝前后的區別,在下一節中,會詳細的來學習代碼簽名認證和策略,并寫一個自己的安全管理器。
public static void main(String[] args){
System.out.println(System.getSecurityManager());
}
運行這個main函數,輸出什么?是的輸出null,這個時候我們沒有安裝默認的安全管理器
重新換個方式運行,在ecplise里右鍵--Run As--Run Configuration--Arguments,在VMarguments的欄目里輸入
-Djava.security.manager。在點擊Run,這個時候看到什么?
輸出:securityManager的對象名。這個時候默認的安全管理器就被安裝上了。
總結:
在java虛擬機中,它在訪問控制-對于外部資源的訪問控制-起到中樞作用
前面第三和第四節我們一直在強調一句話,類裝載器和安全管理器是可以被動態擴展的,或者說,他們是可以由用戶自己定制的,今天我們就是動手試試,怎么做這部分的實踐,當然,在閱讀本篇之前,至少要閱讀過筆記三。
下面我們先來動態擴展一個類裝載器,當然這只是一個比較小的demo,旨在讓大家有個比較形象的概念。
首先定義自己的類裝載器,從ClassLoader繼承,重寫它的findClass方法,至于為什么要這么做,大家如果看過筆記三就知道,雙親委托模式下,如果parent沒辦法loadClass,bootStrap也沒把辦法loadClass的時候,jvm是會調用ClassLoader對象或者它子類對象的findClass來裝載。
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class MyClassLoader extendsClassLoader {
@Override
protected Class<?> findClass(String name) throwsClassNotFoundException {
byte[] data = getByteArray(name);
if (data == null) {
throw new ClassNotFoundException();
}
return defineClass(name, data, 0, data.length);
}
private byte[] getByteArray(String name){
String filePath = name.replace(".", File.separator);
byte[] buf = null;
try {
FileInputStream in = new FileInputStream(filePath);
buf = new byte[in.available()];
in.read(buf);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return buf;
}
}
定義一個類,專門用于被裝載,這里我們定義了一個靜態代碼塊,待會用到它
public class TestBeLoader {
static{
System.out.println("TestBeLoader init");
}
public void sayHello(){
System.out.println("hello");
}
}
定義一個有main函數入口的public類來做驗證
public class TestClassLoaderDemo {
public static void main(String[] args) throws InstantiationException,IllegalAccessException {
Class thisCls = TestClassLoaderDemo.class;
MyClassLoader myClassLoader = new MyClassLoader();
System.out.println(thisCls.getClassLoader());
System.out.println(myClassLoader.getParent());
try {
//用自定義的類裝載器來裝載類,這是動態擴展的一種途徑
Class cls2 =myClassLoader.loadClass("com.yfq.test.TestBeLoader");
System.out.println(cls2.getClassLoader());
TestBeLoader test=(TestBeLoader)cls2.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
查看運行結果
sun.misc.Launcher$AppClassLoader@19821f
sun.misc.Launcher$AppClassLoader@19821f
sun.misc.Launcher$AppClassLoader@19821f
TestBeLoader init
第一個輸出:裝載TestClassLoaderDemo的類是AppClassLoder
第二個輸出:裝載myClassLoader的裝載器也是AppClassLoader,這里也驗證了我們筆記三講的,在同個線程中,動態連接模式會運用當前線程的類加載器來加載所需的class文件,因為第一個和第二個輸出是同一個對象的對象名
第三個輸出:是TestBeLoader的類加載器,這個輸出驗證了,雙親委托模式下的動態連接模式,由于myClassLoader是由AppClassLoader裝載的,所以它會委托自己的parent來裝載com.yfq.test.TestBeLoader這個類,加載成功所以就不再調用自己的findClass方法,這個我們在筆記三有做過簡要的討論。
第四個輸出:如果我們將TestBeLoadertest=(TestBeLoader)cls2.newInstance();這句話注掉,則不會有第四個輸出,為什么?
類的裝載大致分為三步,裝載,連接,初始化。而初始化這一步,是在我們第一次創建對象的時候才進行初始化分配內存,這一點需要注意,并不是class被load內存后就立刻初始化。
安全管理器SecurityManager里設計的內容實在是非常的龐大,它的核心方法就是checkPerssiom這個方法里又調用AccessController的checkPerssiom方法,訪問控制器AccessController的棧檢查機制又遍歷整個PerssiomCollection來判斷具體擁有什么權限一旦發現棧中一個權限不允許的時候拋出異常否則簡單的返回,這個過程實際上比我的描述要復雜得多,這里我只是簡單的一句帶過,因為這里涉及到很多比較后面的知識點。
下面來嘗試一下寫一個非常簡單的demo,旨在讓你有一個比較形象的思維,不會在概念上打轉。
定義一個類繼承自SecurityManger重寫它的checkRead方(如果你有興趣可以先跳到super.checkRead(file, context);看看,當然不看也沒有關系,我們后面的章節會基于這個demo做擴展的時候也會講到)。
public class MySecurityManager extendsSecurityManager {
@Override
public void checkRead(String file) {
//super.checkRead(file, context);
if (file.endsWith("test"))
throw new SecurityException("你沒有讀取的本文件的權限");
}
}
定義一個有main函數的public類來驗證自己的安全管理器是不是器作用了。
import java.io.FileInputStream;
import java.io.IOException;
public class TestMySecurityManager {
public static void main(String[] args) {
System.setSecurityManager(new MySecurityManager());
try {
FileInputStream fis = new FileInputStream("test");
System.out.println(fis.read());
} catch (IOException e) {
e.printStackTrace();
}
}
}
運行代碼查看控制臺輸出
Exception in thread "main" Java.lang.SecurityException:你沒有讀取的本文件的權限
atcom.yfq.test.MySecurityManager.checkRead(MySecurityManager.java:9)
atjava.io.FileInputStream.<init>(FileInputStream.java:100)
atjava.io.FileInputStream.<init>(FileInputStream.java:66)
at com.yfq.test.TestMySecurityManager.main(TestMySecurityManager.java:10)
從上面的異常我們發現,安全管理器起作用了。讀過筆記四的人應該會發現,這里我們用到了一個筆記四提到的方法:System.setSecurityManager(new MySecurityManager());這個是安裝安全管理器的另外一種方法,筆記四中我們曾經用-Djava.security.manager安裝過默認的安全管理器,有印象不?
好了,我們的安全管理器是怎么被執行的呢?如果你有興趣可以繼續往下看一下,也可以跳過,這里只是簡單的介紹一下,也是本人習慣的學習思路
直接跳到FileInputStream的構造函數里,下面貼出代碼,簡單閱讀一下
public FileInputStream(File file) throwsFileNotFoundException {
Stringname = (file != null ? file.getPath() : null);
SecurityManagersecurity = System.getSecurityManager();
if(security != null) {
security.checkRead(name);
}
if(name == null) {
thrownew NullPointerException();
}
fd= new FileDescriptor();
open(name);
}
發現沒?它首先執行SecurityManager security =System.getSecurityManager();然后再調用security的checkRead方法,就是這么簡單。
如果你還有興趣那么繼續往下讀,在使用java的File時,你是否用過setWritable(boolean, boolean),讓你可以指定創建文件的權限,學習了安全管理器之后你有沒有有豁然開朗的感覺,它是怎么實現的,相信你已經猜到了,沒有錯就是安全管理器設置權限啦。下面貼出它的代碼,同時也引入一個新的概念Permission
public boolean setWritable(booleanwritable, boolean ownerOnly) {
SecurityManagersecurity = System.getSecurityManager();
if(security != null) {
security.checkWrite(path);
}
returnfs.setPermission(this, FileSystem.ACCESS_WRITE, writable, ownerOnly);
}
Permisson就是權限的意思,它僅僅取出安全管理器然后將文件的權限設置了一下而已,這個也是后面所有關于權限的一個不可或缺的類!
好了今天的文件安全管理器demo就到這里。意在淺顯易懂!
申明:文章的部分內容有參照網上的其他作者的內容,這里只用來學習交流!
如果你循序漸進的看到這里,那么說明你的毅力提高了,jvm的很多東西都是比較抽像的,如果不找相對應的代碼來輔助理解,其實很難有個比較形象的思維,前面我努力的嘗試通過將概念投射到代碼的方式去講解jvm的各個細節,希望你能夠試著自己也去找到對應的代碼段,然后試著讀一讀。一開始可能沒有那么容易,但是沒有一件事情,一開始就是容易的。
終于到了這一節,這一節,其實相對于筆記二,筆記三和筆記四,是相對比較容易的,即使你對密碼編碼學一竅不通也不妨礙你學習,我們不會涉及到太多的實現,而主要從應用著手,旨在淺顯易懂,觸類旁通。在下一節中,我們會來嘗試做一次簽名,前提是你看完這一節
筆記3的時候我們曾經提到class文件的校驗器,記得它分為幾趟不,四趟,而jar包的代碼簽名認證和class檢驗的第一趟是有聯系的。
class文件校驗器的第一趟會對jar文件的結構,長度等進行校驗,其中也包括對jar的簽名和認證進行校驗。
我們相關的class文件打包成了jar包之后,在傳遞這個jar的時候,如何防止jar不被他人暗中的修改呢?
方案一,可能你會想到對整個jar文件進行加密,這個思路是可行的,但是卻顯得比較笨拙,對每個jar文件都執行加密,需要的時候又要執行解密,不僅浪費時間,效率上也是不可取的。
方案二。對jar包的部分內容進行加密,這個思路好像效率高點,但是對哪一部分進行加密?如果沒有加密的那一部分被修改了怎么確認?這又一個問題。
以上兩種簡單地解決方案雖然看起來簡單但是實施起來都是有困難的,那么有沒有好的方法?
有,在jar文件上hash摘要,什么是hash摘要,這里我不丟書包了,簡單的說hash摘要就是有一個叫hash(String content)的哈希函數,當你傳入內容的時候它都將返回一個獨一無二個的128的hash數值,這樣無論傳入的內容多大,hash摘要的長度是固定的。當然附加到jar文件的最后面時總體上并不會影響jar的結構和傳輸。
只要接收方也擁有這個hash函數,那么將jar的內容進行hash后的值再和附加在jar中的hash值做對比就可以知道jar的內容是否被修改過了,看起來好像完美了,但是如果有意破壞的人把jar和hash都替換成具有破壞性ar文件以及由這個具有破壞性的jar文件進行hash運算的hash值,那么前面做的事情也就都沒有意義了,于是聰明的人類想到了對hash摘要運用私鑰進行加密,這樣只有加密方才能對hash值加密,而解密的那方運用公鑰進行解密,而且它總是知道怎么解密的,我們把對hash摘要進行加密的過程稱之為簽名。這就是jar包簽名的大致過程
好吧,上面引述了那么多,無非是想描述下面圖3-3的過程,如果你看到這個圖完全明白,那前面那段廢話就直接跳過吧!
先不管什么是認證,先來了解一下密碼學的一點小知識
前面我說過,看這篇文章是不需要你有密碼學的知識的,是的,我騙你,至少基本的概念還是要理解過的。如果你完全不懂,不要慌,我舉個簡單的例子來幫你簡單的理解一下一兩個基本的概念。
第一個概念對稱加密,什么是對稱加密?假設A想要說暗語,A想說5的時候就把5*3,然后把5*3的結果15告訴B,因為B知道A說暗語的規則,所以B就把15除以3,知道A要告訴自己5,這就是對稱加密。
第二個概念非對稱加密,假設A要把一句話告訴B,A就把這句話放到一個有兩個完全不同的鎖(lock1,lock2)的箱子里,然后鎖上,A有lock1的鑰匙,把箱子交給B,而B擁有lock2的鑰匙,B通過打開lock2也能看到箱子里的字條,這就是非對稱加密。而A擁用的那把鑰匙叫私要,B擁有的那把鑰匙復制多份之后分給他們組員,就成了公鑰。
沒有那么可怕對吧!而在這里我應該負責任的告訴你,對于hash摘要的簽名用的就是非對稱加密!
回到我們的主題,什么是認證,當我們隊hash摘要用私鑰進行加密,然后把公鑰發給B和B組里的所有人的時候,如果中間傳遞的環節被人偷天換日的將公鑰換掉了,這個時候,jar文件的簽名的真實性又受到了威脅,怎么保證傳遞公鑰的時候,公鑰的真實性,這就是我們提到的認證,我們如果把公鑰交給一個公正的認證機構,認證機構對你的公鑰進行加密之后的序列號,我們就稱為證書,需要公鑰的人得帶證書后向認證機構申請解密,這樣安全性就好很多了。
上面的一堆廢話,其實也是為了描述下面這個圖的整個過程,如果你一眼就看明白下面這個圖,那就忽略上面的描述吧
好吧,這一節的內容全是概念,概念只需要你看而不是要你背,在某個時候你會煥然大悟的,而這個時間應該會是在下一節Java之jvm學習筆記八(實踐對jar包進行簽名)
這一節,以實踐為主,在跟著我做相應的操作之前,我希望你已經能夠理解筆記七所提到的概念,至少你應該對于筆記七的那個大圖有所了解。
好了!對于習慣用ecplise的朋友今天不得不逼迫你把jdk的環境搭建出來!下面讓我們動手來實踐一下對jar進行簽名吧!
首先配置jdk的環境變量,如果你的電腦已經配置了,那直接跳過這一步
path=%JAVA_HOME%/bin
JAVA_HOME=C:/Java/jdk1.6.0_01
CLASSPATH=.;%JAVA_HOME%/lib/dt.jar;%JAVA_HOME%/lib/tools.jar
配置要這幾個jdk的環境參數,好了,配完了,試著在cmd里跑一下Java,javac,看看命令是否生效,如果配置成功執行第二步。
來寫幾個簡單的類,簡單的才是大家的。你完全可以直接copy我的代碼,部分看不懂,忽略它,做實驗而已,對那個jar文件簽名不是簽,這個例子的代碼邏輯是后面才用到的,不用讀
第一個類Doer
public abstract interface Doer {
voiddoYourThing();
}
第二個類
import java.security.AccessController;
import java.security.PrivilegedAction;
import com.yfq.test.Doer;
public class Friend implements Doer{
private Doer next;
private boolean direct;
public Friend(Doer next,booleandirect){
this.next=next;
this.direct=direct;
}
@Override
public void doYourThing() {
System.out.println("Ima Friend");
if(direct) {
next.doYourThing();
}else {
AccessController.doPrivileged(newPrivilegedAction() {
@Override
publicObject run() {
next.doYourThing();
returnnull;
}
});
}
}
第三個類
import java.security.AccessController;
import java.security.PrivilegedAction;
import com.yfq.test.Doer;
public class Stranger implements Doer{
private Doer next;
private boolean direct;
public Stranger(Doer next, boolean direct){
this.next= next;
this.direct= direct;
}
@Override
public void doYourThing() {
System.out.println("Ima Stranger");
if(direct) {
next.doYourThing();
}else {
AccessController.doPrivileged(newPrivilegedAction() {
@Override
publicObject run() {
next.doYourThing();
returnnull;
}
});
}
} }
好了,編譯一下,用強大的ecplise來編譯,項目-右鍵-Build Project(工具是拿來用的,不要浪費這些強大的功能!)
打jar包,用ecplise就可以了就有導出jar包的功能,我還是那句老話,有工具不用,不是牛,是蠢。
步驟一,項目-右鍵-Export-java-JARfile-next
步驟二,展開目錄清單-分別對com.yfq.tes.friend和com.yfq.test.stranger打包(friend.jar,stranger.jar),放到哪里就隨便你了,只要你記得就好,我這里假設是放在d盤的根目錄下
第四步
用java的keytool生成密鑰對,用java的jarsigner做簽名(記得筆記七我們說過對hash摘要的加密是非對稱加密的嗎?這里就需要兩把不同的鑰匙啦),一步步跟我來。
步驟一,cmd窗口,進入到存放friend.jar和stranger.jar的目錄下,假設我的jar文件放在d盤下,直接輸入盤符d:就可以了。
步驟二,在cmd窗口中輸入keytool-genkey -keystore ijvmkeys.keystore -keyalg RSA -validity 10000 -aliasfriend.keystore
生成第一個密鑰對,這個密鑰對的別名是 friend.keystore,采用的加密算法為RSA,密鑰對的過期時間是10000天,密鑰對存儲的文件名ijvmkeys.keystore,而查看ijvmkeys.keystore的密碼和friend.keystore密鑰對的查看密碼我們設置為123456
注意:這里在設置名字和姓氏的時候要特別的注意,不要隨便的亂寫,否則將導致后面的簽名失敗,一般我們寫完網絡域名的形式如:www.keycoding.com這樣。
步驟三,在cmd窗口輸入,keytool-genkey -keystore ijvmkeys.keystore -keyalg RSA -validity 10000 -aliasstranger.keystore
按照步驟2的截圖,一步一步輸入吧,這個步驟是生成別名為stranger.keystore的密鑰對。
好了密鑰對生成結束,看看你的jar文件目錄下有沒有多出一個文件ijvmkeys.keystore,是滴,這里生成了一個用于存放密鑰對的文件。
步驟四,查看生成的密鑰文件,在cmd窗口輸入keytool -list -v -keystore ijvmkeys.keystore
步驟五,對jar進行摘要并對hash摘要進行加密生成簽名,放置到jar文件結構的尾部
在cmd窗口輸入
jarsigner -verbose -keystoreijvmkeys.keystore friend.jar friend.keystore
jarsigner -verbose -keystoreijvmkeys.keystore stranger.jar stranger.keystore
步驟六,右鍵frend.jar和stranger.jar用rar解壓器看看它們在META-INF目錄下是否生成了兩個附加的文件
而關于這兩個附加文件的用處,我這里也簡單的說明一下,首先從名字上來講他是八個字符,他默認取我們的密鑰對的名字的前八個字符做名字而因為我們的密鑰對名字是friend.keystore所以生成的名字將點替換為下滑線。如果你想要自己指定名字在keytool后面加上-sigFile XXXX這個參數
另外FRIEND_K.SF這個文件我們簡單的展開
Signature-Version: 1.0
SHA1-Digest-Manifest-Main-Attributes:QHukAYw2MtCop4vlrhjJDDro1fQ=
Created-By: 1.6.0_12 (Sun MicrosystemsInc.)
SHA1-Digest-Manifest:YePdyFc1+FVdY1PIcj6WVuTJAFE=
Name:com/yfq/test/friend/Friend$1.class
SHA1-Digest:mj79V3+YKsRAzxGHpyFGhOdY4dU=
Name: com/yfq/test/friend/Friend.class
SHA1-Digest:tqPfF2lz4Ol8eJ3tQ2IBvvtduj0=
它包含了簽名的版本,簽名者,還有被簽名的類名,以及這個類的hash摘要,第四行是整個本文件的摘要,用于jar包的校驗
FRIEND_K.DSA 文件,SF 文件被簽名且簽名被放入.DSA文件。.DSA文件還包含來自密鑰倉庫的證書或證書鏈(被編碼到其中),它們鑒別與用于簽名的私鑰對應的公鑰。
步驟七,校驗jar包在cmd中輸入jarsigner -verify friend.jar和jarsigner-verify stranger.jar
到這里jar簽名的實驗已經完畢!!!!!
查看上面步驟四截圖,我們來驗證一下在筆記七里說過的話。
1.我們說過hash摘要是一個128的值,對不對呢,看證書指紋那一行,md5:....
你數一數總共有幾個十六進制數,32個,一個十六進制數用4個位可以表示完,那么總共是幾位,32*4=128,但是后面還有一個sha1的,怎么回事他貌似不止128位,是滴,散列函數多種多樣,到底用那個散列函數,md5還是sha1這個就看你喜歡,而要使用哪個散列函數是可以指定的,keytool的參數-keyalg "DSA",這個參數就是用來指定用什么散列算法的,默認的就是DSA,普通的128位散列數已經是安全的了。
2.在 筆記七中,記不記得最下面那個圖,有一個認證機構會對解密簽名(被加密的hash摘要)的公鑰做認證(也就是加密公鑰),并發布證書,我們這里沒有認證機構,你有沒有這個疑問?
keytool程序在生成密鑰時,總是會生成一個自簽名證書(自簽名是指:如果附近沒有認證機構,可以用私鑰對公鑰簽名,生成一個自簽名證書)
通過本章我們學習對一個jar進行簽名,一個jar可以同時被多個機構或作者簽名,看起來實驗很復雜其實很簡單。如果你還想了解更多關于jar包簽名的知識,本人在這里推薦一篇文章(http://blog.csdn.net/yangxt/article/details/1796965),本人自己在學習jar包簽名的時候也從這篇文章中收益匪淺,希望它對你有幫助。
什么是Java的策略,什么又是策略文件。
今天我換一下筆記的方式,不是直接講概念,而是先來做一個小例子,相信你做完這個例子之后再看我對例子的講解,你對策略,策略文件,會豁然開朗的感覺。
例子很簡單,簡單的才是大家的,下面跟著我(你完全可以copy我的代碼)。
定義一個簡單類。
package com.yfq.test;
import java.io.FileWriter;
import java.io.IOException;
public class TestPolicy {
public static void main(String[] args) {
FileWriter writer;
try {
writer = newFileWriter("d:/testPolicy.txt");
writer.write("hello1");
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
定義一個簡單的策略文件,我們放到工程的類路徑下(src文件夾里),名字為myPolicy.txt
grant codeBase"file:D:/workspace/TestPolicy/bin/*" {
permission java.io.FilePermission "d:/testPolicy.txt","read";
};
我簡單的來說一下這個文件的作用
第一行:grant codeBase"file:D:/workspace/TestPolicy/bin/*"意思是給D:/workspace/TestPolicy/bin/*給這個路徑下的所有文件定義權限,星號是統配符,所有的意思
第二行:permission java.io.FilePermission"d:/testPolicy.txt", "read";意思是d:/testPolicy.txt這個文件只分配讀的權限。
運行,在cmd窗口輸入(運行不起來,說明jdk的環境變量沒有配置好,去配一下)
java -classpath D:/workspace/TestPolicy/bin-Djava.security.manager-Djava.security.policy=D:/workspace/TestPolicy/src/myPolicy.txtcom.yfq.test.TestPolicy
這句話的意思,把當前的類路徑指定為D:/workspace/TestPolicy/bin,啟動默認的安全管理器(這里你應該也猜到了,策略必須和安全管理器一起合作才能起作用),設置安全策略文件的位置(關于策略文件的安裝是有多種方式的,這里我們是在windows下,如果你有興趣可以自己再多摸索)。
查看輸出
這里報出了異常,提示本應用對d:/testPolicy.txt這個文件沒有寫的權限。
修改一下上面的myPolicy.txt文件,如下
grant codeBase "file:D:/workspace/TestPolicy/bin/*"{
permission java.io.FilePermission "d:/testPolicy.txt","read,write";
};
再次運行,沒有報錯了。
好了實驗成功,或許你會疑問,這個有鳥用啊,不要急,在下一節中,我們會詳細的講,現在我做一下簡單的介紹,這個策略文件(本文中為myPolicy.txt)在java中對應著一個類,叫java.security.Policy(策略),這是一個神奇的類,有了它,你可以定義自己代碼的權限,當然它還可以結合我們筆記四講到的安全管理器。而你現在只需要記住一句話:
java對應用程序的訪問控制策略是由抽象類java.security.Policy的一個子類的單例所表示,任何時候,每個應用程序實際上只有一個Policy對象,Policy對象對應著策略文件。類裝載器利用這個Policy對象來幫助他們決定,在把一段代碼導入虛擬機時應該給予什么權限。
如果你之前有稍微聽過策略這個概念,希望看完本文有給你豁然開朗的感覺
前面一節,我們做了一個簡單的實驗,來說明什么是策略文件,在文章的最后,也順帶的講了一下什么是策略,還有策略的作用。
為了引出另外一個很重要的概念ProtectionDomain(保護域),所以我們還是要先來回顧一下什么是策略。
首先,什么是策略,今天的東西純粹是比較概念的。當然,如果你讀過筆記九,今天的東西,就真的是soso
Java對應用程序的訪問控制策略是由抽象類java.security.Policy的一個子類的單例所表示,任何時候,每個應用程序實際上只有一個Policy對象,Policy對象對應著策略文件。類裝載器利用這個Policy對象來幫助他們決定,在把一段代碼導入虛擬機時應該給予什么權限。
上面那段話告訴我們一個應用程序對應一個策略對象,一個策略對象對應一個策略文件。
那么策略文件,除了對我們筆記九中一個文件夾下的所有文件起限制作用外還能對什么主體起作用呢?先來看看下面的策略文件myPolicy.txt
keystore "ijvmkeys";
grant signedby "friend" {
permission java.io.FilePermission "d:/testPolicy.txt", "read,write";
};
grant signedby "stranger" {
permission java.io.FilePermission "d:/testPolicy.txt","read,write";
};
grant codeBase"file:D:/workspace/TestPolicy/bin/*" {
permission java.io.FilePermission "d:/testPolicy.txt","read,write";
};
簡單的解讀一下
第一行:keystore "ijvmkeys",這一行的意思,密鑰對存放在當前目錄一個叫ijvmkeys的文件里(記得筆記八做過的jar包簽名實驗嗎)
第二行:grant signedby "friend",grant是授權的意思,這一行的意思是,給一個被“friend”的密鑰對簽名的文件授權
第三行:permission java.io.FilePermission"d:/testPolicy.txt", "read,write";這行的意思是對于d:/testPolicy.txt賦予讀寫的權限
倒數第三行:grant codeBase"file:D:/workspace/TestPolicy/bin/*" 這一句我們筆記九的時候見過,就是對D:/workspace/TestPolicy/bin/*下的所有文件賦予權限。
重點一:到這里我們應該可以知道,策略文件可以給一系列被簽名的代碼庫(“friend”,‘stranger“都是代碼庫)授權,也可以給一個代碼來源(一個具體的路徑或者說url就是一個代碼來源)授權。
重點二:策略文件不僅可以存儲在文件中(后綴名是什么不重要),還可以存放在數據庫里。
到了這里我們對策略有一個比較完整的概念了,但是你有沒有這么一個疑問,前面我們總說,一個應用程序對應一個策略單例,一個策略單例對應一個策略文件,它到底怎么對應的?下面我們就來探究一下。
在探究之前,我們先引入一個新的概念叫保護域(ProtectionDomain),在筆記三的時候,我們提到過類裝載器將class文件load內存的時候會將它放置到一個保護域中,是滴今天我就來說說什么是保護域。
當類裝載器將類型裝入Java虛擬機時,它們將為每個類型指派一個保護域。保護域定義了授予一段特定代碼的所有權限。(一個保護域對應策略文件中的一個或多個Grant子句。)裝載入Java虛擬機的每一個類型都屬于一個且僅屬于一個保護域。
類裝載器知道它裝載的所有類或接口的代碼庫和簽名者。它利用這些信息來創建一個CodeSource對象。它將這個CodeSource對象傳遞個當前Policy對象的getPermissions()方法,得到這個抽象類java.security.PermissionCollection的子類實例。這個PermissinCollection包含了到所有Permission對象的引用(這些Permission對象由當前策略授予指定代碼來源)。利用它創建的CodeSource和它沖Policy對象得到的PermissionCollection,它可以實例化一個新的ProtectDomain對象。它通過將合適的ProtectionDomain對象傳遞給defineClass()方法,來將這段代碼放到一個保護域中
如果你對上面這段話理解不了,看下面這個圖
好了看完上面的這整個過程之后你是否已經理解什么是保護域了。
下面我們再整理一下今天的內容,概念有點多,一個一個的來。
codeSource:代碼源,這個是類裝載器生成的java.security.CodeSource的一個對象,classLoader通過讀取class文件,jar包得知誰為這個類簽過名(可以有多個簽名者,關于簽名請查看筆記七和八)而封裝成一個簽名者數組賦給codeSource對象的signers成員,通過這個類的來源(可能來自一個本地的url或者一個網絡的ur,對應了grant筆記九里myPollicy里的"friend"或者file::....l)賦給codeSource的location成員,還有這個類的公鑰證書賦給codeSource的certs成員(通常一個jar是能夠被多個團體或者機構擔保的,也就是我們說的認證,在java1.2的默認安全管理器還有訪問控制體系結構都只能對證書起作用,而不能對赤裸的公鑰起作用,而實際上,我們用keytool生成密鑰對時,同時會生成一個自簽名證書,所以keytool生成的密鑰對并不是赤裸的)。如果你有疑問,我們看一下jdk里的代碼
public class CodeSource implementsjava.io.Serializable {
private static final long serialVersionUID = 4977541819976013951L;
/**
* The code location.
*
* @serial
*/
private URL location;//本地代碼庫
/*
* The code signers.
*/
private transient CodeSigner[] signers = null;//簽名者
/*
* The code signers. Certificate chains are concatenated.
*/
private transient java.security.cert.Certificate certs[] = null;//證書
Policy:策略,就是用來讀取策略文件的一個單例對象,通過傳入的CodeSource對象(由于codeSource對象里包含了簽名者和代碼來源)所以他通過讀取grant段,取出一個個的Perssiom然后返回一個PerssiomCollection。這個類里有一個很重要的成員變量
// Cache mapping ProtectionDomain to PermissionCollection
private WeakHashMap pdMapping;
這個成員為什么重要,我們來看一個方法
private static void initPolicy (final Policyp) {
......
if (policyDomain.getCodeSource() != null) {
.......
synchronized (p.pdMapping) {
// cache of pd topermissions
p.pdMapping.put(policyDomain,policyPerms);
}
}
return;
}
我們主要看關鍵代碼。這個pdMapping就是把保護域對象當做key將權限集合當做value存在在了這個map里。所以我們說一個保護域對應多個策略文件的grant子句的permission。
ProtectionDomain:保護域,前面我們已經介紹過了,他就是用來容納class文件,還有perssiom,codeSource的一個對象,如果你對此還有什么疑問,我們也看看它的代碼,來驗證一下我們的結論
public class ProtectionDomain {
/* CodeSource */
private CodeSource codesource ;//代碼源
/* ClassLoader the protection domain was consed from */
private ClassLoader classloader;//類裝載器
/* Principals running-as within this protection domain */
private Principal[] principals;
/* the rights this protection domain is granted */
private PermissionCollection permissions;//權限集合
Permission:權限,這個對應了我們筆記九里的grant子句里的一個permission,它的結構也很簡單,權限名和動作,就好像我們筆記九里的java.io.FilePermission是一個權限名
而動作則是read和write,在Permission中它對應一個字符串。
現在我們用一張圖來把上面幾個概念串聯起來
到這里我們已經有一條比較完整的思路了,從筆記四到這一節的筆記十,我們所要說的都只有一件事情,類裝載器在裝載類的時候(或者執行類)會調用安全管理器,安全管理器,則通過判斷策略來判斷我們是不是允許加載這個類,或者執行某些操作,允許某個文件的讀寫啊之類的(這個在筆記九的時候我們已經做過實驗了)。那么你有沒有這樣的疑問,到底安全管理器是怎么樣去調用策略的?這里我們不得不提出一個新的概念訪問控制器AccessControl,如果你想知道訪問控制器是干什么的,做什么工作,怎么和安全管理進行合作,那么請你閱讀下一節。
這一節,我們要學習的是訪問控制器,在閱讀本節之前,如果沒有前面幾節的基礎,對你來說可能會比較困難!
知識回顧:
我們先來回顧一下前幾節的內容,在筆記三的時候我們學了類裝載器,它主要的功能就是裝載類,在裝載的前后,class文件校驗器會對class文件進行四趟的校驗,而第一趟的校驗會對文件的結構進行校驗,對文件的結構完整性的校驗時會校驗class文件的hash摘要是否一致以確定文件沒有中途被修改過,所以基于class文件校驗我們又學習了jar的認證和簽名,當class文件被裝載到內存的時候,一個應用啟動時,jvm會為該應用生成一個Policy的單例對象,它用于讀取策略文件的grant信息,當類裝載器裝載一個類的時候,它根據jar包中的簽名信息、證書、jar的url信息生成一個CodeSource對象,CodeSource對象向Policy對象索要一個PermissionCollecion權限集合,它是由各個grant子句中的permission語句的實例映射,再由CodeSource對象、PermissionCollecion權限集合、類加載器交由類加載器的defineClass方法組成了ProtectionDomain保護域。最后class字節碼在內存中被放在了這個保護域中。
是的內容非常的多,概念也非常的多,所以如果你對前面的知識回顧一頭霧水,建議還是倒回去把那些基礎的概念再補一補。
回顧完目前為止的所有知識之后,我們需要解決兩個問題
第一,什么是訪問控制器。
第二,它是怎么樣和安全管理器配合工作的。
我們先來簡單的回答第一個問題,你可以聽不明白,但是如果你耐性的往下看,在我回答第二個問題的時候,我們會做幾個比較復雜的demo,而這些復雜的demo,會在無形之中讓你真正的認識到什么是訪問控制器。在文章的最后如果篇幅夠的話我們也會帶大家來讀一讀jdk里的源碼,看看他和安全管理是怎么配合工作的。
類Java.security.AccessControler提供了一個默認的安全策略執行機制,他使用棧檢查機制來決定潛在的不安全操作是否被允許。這個訪問控制器不能夠被實例化,它不是一個對象,而是集合在單個類的多個靜態方法。AccesControler最核心的方法是checkPermission,這個方法決定一個特定的操作是否被允許,他接收一個Perssmission的子類對象,當AccessControler確定操作被允許,它將簡單的返回,而如果操作被禁止,它將異常中止,并拋出一個ACSSessControlException,或者是它的子類。
關于什么是訪問控制器,聽不明白,不要著急,下面我們先來做一個簡單地demo,這個demo主要是為了后面我們來實現一個自己的AceessControler做準備,是關于implies這個方法理解,這個方法可以說是串聯起我們所有內容的核心。
public static void main(String[] args){
Permission perOne = newFilePermission("d:/tmp/test.txt",SecurityConstants.FILE_READ_ACTION);
Permission perAll = newFilePermission("d:/tmp/*",SecurityConstants.FILE_READ_ACTION);
System.out.println(perOne.implies(perAll));
System.out.println(perAll.implies(perOne));
}
輸出的結果為:
false
true
說明:implies方法就是用于判斷一個權限的范圍是不是包含了另外一個權限的范圍,在這個demo里,我們試著去判斷對于perAll的權限是否包含perOne的權限還有perOne的權限是否包含perAll權限,很顯然,perAll權限是包含perOne的。而實際上AccessControler里有一個權限棧,它就是遍歷棧幀中的PermissionCollecion里的每個Permission然后調用里Permission的implies來判斷是否包含某個權限的。
下面我們來做另外的一個demo,這個demo我們采取累加型的方法一點點的添加代碼,以讓你了解整個AccessControler和SecurityManager是怎么配合著工作的,這個demo稍微會復雜一點
試著實現自己的安全管理器,實驗是否成功,以下主要分三步來完成
第一步:實現一個自己的類MySecurityManager,它繼承自SecurityManager,重寫它的checkRead方法,我們直接讓他拋出一個SecurityException異常。(copy吧少年,要的是你知識的儲備,不是要你把代碼背下來),
public class MySecurityManager extendsSecurityManager {
@Override
public void checkRead(String file) {
//super.checkRead(file, context);
throw new SecurityException("你沒有的權限");
}
}
第二步:實現一個簡單的類,主要用來測試我們自己定義的安全管理器起作用了沒有,我們這里借助了FileInputStream,因為FileInputStream會調用安全管理器去校驗權限(我們在筆記六已經詳細的講解過),所以用FileInputStream測試我們自己的安全管理器非常的適合。
import java.io.FileInputStream;
import java.io.IOException;
import java.security.ProtectionDomain;
public class TestMySecurityManager {
public static void main(String[] args) {
System.setSecurityManager(new MySecurityManager());
try {
FileInputStream fis = new FileInputStream("test");
} catch (IOException e) {
e.printStackTrace();
}
}
}
現在簡單的說明一下:
1.TestMySecurityManager的main函數第一行其實就是注冊我們自己的安全管理器(還有一種安裝安全管理器的方式,記得不,如果你忘記了請你看看筆記六)
2.FileInputStream fis = newFileInputStream("test");這一行創建了一個FileInputStream對象,這個構造器內部會調用 public FileInputStream(File file);這個構造器,而這個構造會調用Ststem.getSercurityManager來取得當前的安全管理器security,然后調用它的checkRead方法來校驗權限。由于我們在第一行注冊了自己的安全管理器,所以它將調用我們自己的安全管理器的checkRead來執行校驗。
第三步:運行程序
Exception in thread "main"java.lang.SecurityException: 你沒有的權限
at com.yfq.test.MySecurityManager.checkRead(MySecurityManager.java:8)
at java.io.FileInputStream.<init>(FileInputStream.java:100)
at java.io.FileInputStream.<init>(FileInputStream.java:66)
at com.yfq.test.TestMySecurityManager.main(TestMySecurityManager.java:11)
好了,到這里說明我們自己的安全管理器安裝上去了。上面的異常正好是我們期望見到的。
我們來實現一個自己的類MyFileInputStream(當然這個不是真正意義的字節流包裝類),它用于取代FileInputStream,它可以模擬FileInputStream是怎么去調用安全管理器,怎么去執行校驗的。
第一步:編寫MyFileInputStream(copy吧少年,不要自己狂敲)
import java.io.File;
import java.io.FileNotFoundException;
public class MyFileInputStream {
public MyFileInputStream(String name) throws FileNotFoundException{
this(name != null ? new File(name) : null);
}
public MyFileInputStream(File file) throws FileNotFoundException {
String name = (file != null ? file.getPath() : null);
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
}
}
}
簡單的說一下邏輯,這個類MyFileInputStream(String name)的構造函數調用MyFileInputStream(File file)這個構造函數,而MyFileInputStream(Filefile)這個構造函數通過System.getSecurityManager();取出當前的SecurityManager,然后調用它的checkRead方法。是滴,這個其實是FileInputStream源碼里的邏輯,我只是把一些有妨礙我們理解的代碼去掉了而已。
第二步,修改步驟一里的TestMySecurityManager里的main用自己的類替換FileInputStream函數如下
import java.io.IOException;
public class TestMySecurityManager {
public static void main(String[] args) {
System.setSecurityManager(new MySecurityManager());
try {
MyFileInputStream fis = new MyFileInputStream("test");
} catch (IOException e) {
e.printStackTrace();
}
}
}
第三步,運行程序,好吧如果你用ecplise那么肯定報錯,看看這個錯誤
居然找不到我們的類,你郁悶沒有,即使你跑到TestMySecurityManager.class的目錄下,再運行還是這個問題。
我就不賣關子了,還是環境變量沒有設置好。這里涉及到一些比較基礎的問題,我簡單的提一下,不然可能永遠都講不完了
我們知道配置jdk的環境的時候我們總是習慣設置三個變量
path=%JAVA_HOME%/bin
JAVA_HOME=C:/Java/jdk1.6.0_01
CLASSPATH=.;%JAVA_HOME%/lib/dt.jar;%JAVA_HOME%/lib/tools.jar
這三個變量代表什么意思呢?
path,其實就是我們的java工具的目錄,像我們編譯java文件用到javac,還有運行class文件用到的java命令,包括我前面見到的密鑰生成工具keytool和簽名工具jarsigner。都是在這個path被配置的前提下才能正常運行的。
JAVA_HOME這個僅僅是一個變量名,你喜歡改成別的名字也可以,只是調用它的地方需要作出對應的修改
CLASSPATH:這個就是引起我們現在問題的地方,我們知道類加載器會加載類,但是它如何知道到哪里去加載類,這個路徑就是告訴類加載器class文件放在了那個地方。
好了既然是這樣的話,我們來設置一下CLASSPATH,
第四步,設置CLASSPATH.到com.yfq.test.TestMySecurityManager所在的編譯目錄
在cmd窗口我們輸入java-classpath D:/workspace/MySecurityManager/bincom.yfq.test.TestMySecurityManager。
查看控制臺輸出
報錯的提示變了,它提示MyFileInputStream這個類找不到,但是它命名和com.yfq.test.TestMySecurityManager在同個編譯目錄下,為什么?
好吧,這里我就不繞彎子了,我們再修改com.yfq.test.TestMySecurityManager,將設置自己的安全管理器的那行先簡單地注釋掉如下
import java.io.IOException;
public class TestMySecurityManager {
public static void main(String[] args) {
//System.setSecurityManager(new MySecurityManager());
try {
MyFileInputStream fis = new MyFileInputStream("test");
} catch (IOException e) {
e.printStackTrace();
}
}
}
編譯之后再執行java -classpath D:/workspace/MySecurityManager/bincom.yfq.test.TestMySecurityManager,沒有報錯了。為什么會這樣子???
重點:講了這么一大篇幅,我無非要告訴你,在一般的情況下,同個線程中,我們用的是同一個類加載器去動態加載所需要的類文件,但是,如果我們設置了SecurityManager的時候,情況就不一樣了,當我們設置了安全管理器之后,當前類由于需要用到安全管理器來判斷當前類是否有加載類MyFileInputStream的權限,所以當前類會委托SecurityManager來加載MyFileInputStream,而對于SecurityManger來說它就從CLASSPATH指定的路徑加載我們的類,所以它沒有找到我們的MyFileInputStream類。
第五步,解決SecurityManager加載類,找不到類的問題。
解決方案太多了,第一種方法:直接修改系統的配置CLASSPATH將MyFileInputStream所在的類加到CLASSPATH中,但是這樣太笨了。
第二種方法:直接使用set classpath命令,我們執行這個命令set classpath=.;D:/workspace/MySecurityManager/bin;%classpath%再執行
java com.yfq.test.TestMySecurityManager,問題解決。
第三種方法 : java -cp "C:/ProgramFiles/Java/jdk1.6.0_12/lib/tools.jar";"C:/ProgramFiles/Java/jdk1.6.0_12/lib/dt.jar";"D:/workspace/MySecurityManager/bin";.com.yfq.test.TestMySecurityManager
第四種方法:java -classpath "C:/ProgramFiles/Java/jdk1.6.0_12/lib/tools.jar";"C:/ProgramFiles/Java/jdk1.6.0_12/lib/dt.jar";"D:/workspace/MySecurityManager/bin";.com.yfq.test.TestMySecurityManager
第六步,將com.yfq.test.TestMySecurityManage中的System.setSecurityManager(new MySecurityManager());前的注釋符號去掉再運行
好了,終于完整的按照我們期望執行了。
上面的步驟一和步驟二,忽略整個調試的過程的話,其實思路很清晰了:
1.注冊我們的安全管理器
2.實例化一個我們自己的類,這個類調用安全管理器的checkRead方法校驗自己有沒有相應的權限
3.MySecurityManager的checkRead方法由于只跑出一個異常,所以直接退出了程序。
這個過程其實就是我們的每個類調用安全管理器的過程,是一個比較簡單的模擬,好好的玩味一下,然后開始我們的步驟三
到了這里,我們只是做了步驟一和步驟二,是不是一個很艱苦的過程?后面不難,真的不難,雖然我一直這么說,簡單的才是大家的,但是難的才是自己的,哈哈哈。
實現我們的AccessControler(終于到這一步了,是不是很期待)
第一步,實現一個類MyAccessControler,并實現一個叫checkPermission的靜態方法。由于AccessControler是一個final類所以我們無法想實現自己的MySecurityManager那樣去繼承它的父類,所以我們就自己定義一個類。
importjava.security.AccessControlException;
importjava.security.Permission;</p><p>
public class MyAccessControler {
public static void checkPermission(Permission perm)
throws AccessControlException
{
throw new SecurityException("你沒有的權限");
}
}
第二步,修改MySecurityManage,重寫父類SecurityManager的checkRead方法和checkPermission方法如下
import java.io.FilePermission;
import java.security.Permission;
import sun.security.util.SecurityConstants;
public class MySecurityManager extendsSecurityManager {
@Override
public void checkRead(String file) {
checkPermission(new FilePermission(file,
SecurityConstants.FILE_READ_ACTION));
}
@Override
public void checkPermission(Permission perm) {
MyAccessControler.checkPermission(perm);//調用我們自己的訪問控制器
}
}
第三步:運行,在cmd控制臺輸出:java-cp "C:/ProgramFiles/Java/jdk1.6.0_12/lib/tools.jar";"C:/ProgramFiles/Java/jdk1.6.0_12/lib/dt.jar";"D:/workspace/MySecurityManager/bin";.com.yfq.test.TestMySecurityManager
恭喜你,哈,報錯了,而且是一個很不常見的錯誤,類循環加載錯誤,你一定很好奇,怎么會循環加載錯誤,這個問題很多人定義自己的安全管理器的時候都遇到過,但是它是怎么產生的?下面我們來改一行代碼,再看它的錯誤信息,你就知道它是怎么產生的了,接著第四步。
第四步,修改上面的MySecurityManage類的checkPermission方法。如下
import java.io.FilePermission;
import java.security.Permission;
import sun.security.util.SecurityConstants;
public class MySecurityManager extendsSecurityManager {
@Override
public void checkRead(String file) {
checkPermission(new FilePermission(file,
SecurityConstants.FILE_READ_ACTION));
}
@Override
public void checkPermission(Permission perm) {
//MyAccessControler.checkPermission(perm);
try {
Class<?>clazz=this.getClass().getClassLoader().loadClass("com.yfq.test.MyAccessControler");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
在次在cmd控制臺輸入運行命令:java-cp "C:/Program Files/Java/jdk1.6.0_12/lib/tools.jar";"C:/ProgramFiles/Java/jdk1.6.0_12/lib/dt.jar";"D:/workspace/MySecurityManager/bin";.com.yfq.test.TestMySecurityManager
報錯很長無止境,這里我們截取了重復的報錯內容出來,看到Exception inthread "main" java.lang.StackOverflowError,棧溢出了,你仔細看報錯
發現:at com.yfq.test.MySecurityManager.checkPermission(MySecurityManager.java:20)
at com.yfq.test.MySecurityManager.checkRead(MySecurityManager.java:11)
這兩句話重復的在出現,一直重復,為什么???
解釋:
記不記得前面那個MyFileInputStream的ClassNotFoundException的,它是怎么引起的,由于我們每new一個MyFileInputStream的時候就要委托我們的SecurityManager調用checkPermission來校驗當前線程是否有加載MyFileInputStream這個類的權限,而SecurityManager的checkPermission方法里我們又調用了Class<?>clazz=this.getClass().getClassLoader().loadClass("com.yfq.test.MyAccessControler");,類裝載器裝裝載類的時候會判斷該類有沒有被裝載的權限,這樣當前的線程棧又需要委托當前的SecurityManager來校驗我們當前的線程是否有裝載com.yfq.testMyAccessControler的權限,又需要再調用CheckPermission這樣就沒完沒了了。
所以問題歸到底還是SecurityManager的問題,它的CheckPermission每次都會被調用來校驗權限問題,一旦在CheckPermission中調用一些非核心API(默認為SecurityConstants.ALL_PERMISSION)的方法時就需要被校驗權限,一不小心就形成遞歸調用直到棧溢出。
現在又有一個新的疑問出來了,第三步中不是棧溢出啊,第四步講一堆干嘛用啊,沒錯,好像是沒什么用,但其實我們是在模擬這個過程,第四步之所以是棧溢出是因為:com.yfq.test.MyAccessControler它永遠沒有機會被load到內存,因為它一直遞歸的被校驗,而第三步則不是,在第一裝載的時候,由于我們的主線程,也就是TestMySecurityManager的main函數開啟的線程它是由sun.misc.Launcher$AppClassLoader這類裝載的,第一次調用CheckPermission的時候,其實我們已經將com.yfq.test.MyAccessControler裝載入內存,而我們前面說過在裝載之前它會委托SecurityManager來裝載要應用類,順便校驗執行權限,所以SecurityManager調用checkPermission的時候由于又被要求裝載MyAccessControler,所以SecurityManager用裝載自己的parent來裝載這個類,按照我們筆記三類裝載器的體系結構,我們知道,類的裝載會采取雙親委托模式,照理來說這個錯誤是不應該發生的,是滴,你的想法是對滴,這貌似是jvm應該要為我們做的事情,但是由于在類執行鏈接的時候MyAccessControler的調用觸發了下一次checkPermission鏈接MyAccessControler所以它的鏈接關系就變成了MyAccessControler<-->MyAccessControler這樣就形成了雙向的鏈接關系,即java.lang.ClassCircularityError,這個是jdk6.0的一個“bug”(我認為是bug)。
不信的話,我們來做個試驗,復制下面的代碼,跑一下
import java.security.Permission;
public class Bug {
public static class A {}
public static void main(String[] args) throws Exception {
System.out.println("Setting SecurityManager");
System.setSecurityManager(newSecurityManager() {
public void checkPermission(Permissionp) {
new A();
}
});
System.out.println("Postset.");
}
}
運行一下
好吧,它就是個可惡的bug,每次new它都要來鏈接一次,這樣就出現循環鏈接了,那么我們如何來解決這個bug呢?(如你把上面的式樣程序 newA()改成new Bug()思考一下,為什么這個我們說的“bug”為什么會不見了)
第五步,再修改上面的MySecurityManage類的checkPermission方法。如下
import java.io.FilePermission;
import java.security.Permission;
importsun.security.util.SecurityConstants;
public class MySecurityManager extendsSecurityManager {
private boolean isLoaded=true;
@Override
public void checkRead(String file) {
checkPermission(new FilePermission(file,
SecurityConstants.FILE_READ_ACTION));
}
@Override
public void checkPermission(Permission perm) {
//MyAccessControler.checkPermission(perm);
if(isLoaded){
isLoaded=false;
System.out.println(MyAccessControler.class.getClassLoader());
}
}
}
再次在cmd中輸入:java -cp"C:/Program Files/Java/jdk1.6.0_12/lib/tools.jar";"C:/ProgramFiles/Java/jdk1.6.0_12/lib/dt.jar";"D:/workspace/MySecurityManager/bin";.com.yfq.test.TestMySecurityManager
親切的畫面有木有!!!!!
第六步,對MyAccessControler中的checkPermission做簡單的實現
import java.io.FilePermission;
importjava.security.AccessControlException;
import java.security.Permission;
import sun.security.util.SecurityConstants;
public class MyAccessControler {
private MyAccessControler() {
super();
}
public static void checkPermission(Permission perm)
throws AccessControlException
{
Permission perAll = newFilePermission("d:/tmp/*",SecurityConstants.FILE_READ_ACTION);
if(perAll.implies(perm)){
System.out.println("你可以讀取這個文件哦!");
}else{
throw new AccessControlException("你沒有讀取這個文件的權限");
}
}
}
修改TestMySecurityManager中的main如下
import java.io.IOException;
public class TestMySecurityManager {
public static void main(String[] args) {
System.setSecurityManager(new MySecurityManager());
try {
MyFileInputStream fis = new MyFileInputStream("d:/tmp/test.txt");
} catch (IOException e) {
e.printStackTrace();
}
}
}
運行:
到這里,我們基本上已經走順了安全管理器MySecurityManager和訪問控制器MyAccessControler是怎么配合工作的了,它大致是這么一個過程,需要權限控制的類會new一個Permission的子類對象(我們的例子里采用MyFileInputStream)然后傳遞給我們的安全控制器里(我們的例子里自己定義了一個MySecurityManager)的checkPermission方法,而這個方法什么也不干就是調用AccessControler的靜態方法checkPermission,我們自己的MyAccessControler里的checkPermission方法我們只是簡單的調用了Permission的implies方法,而,其實這也正是整個java虛擬機安全校驗的總體脈絡,但是這里我們還有一個疑問,AccessControler它到底是怎么和我們筆記九和筆記十的策略和策略文件配合工作的呢???請看下一節,訪問控制器的棧校驗機制
這一節,我們會簡單的描述一下jvm訪問控制器的棧校驗機制。
這節課,我們還是以實踐為主,什么是棧校驗機制,講一百遍不如你自己實際的代碼一下然后驗證一下,下面我們下把環境搭起來。
配置系統環境。
path=%JAVA_HOME%/bin
JAVA_HOME=C:/Java/jdk1.6.0_01
CLASSPATH=.;%JAVA_HOME%/lib/dt.jar;%JAVA_HOME%/lib/tools.jar
配置一個策略文件的運行環境。
第一個類Doer:
public abstract interface Doer {
void doYourThing();
}
第二個類Friend:
import java.security.AccessController;
import java.security.PrivilegedAction;
import com.yfq.test.Doer;
public class Friend implements Doer{
private Doer next;
private boolean direct;
public Friend(Doer next,boolean direct){
this.next=next;
this.direct=direct;
}
@Override
public void doYourThing() {
System.out.println("Im a Friend");
if (direct) {
next.doYourThing();
} else {
AccessController.doPrivileged(new PrivilegedAction() {
@Override
public Object run() {
next.doYourThing();
return null;
}
});
}
}
}
第三個類Stranger:
import java.security.AccessController;
import java.security.PrivilegedAction;
import com.yfq.test.Doer;
public class Stranger implements Doer {
private Doer next;
private boolean direct;
public Stranger(Doer next, boolean direct) {
this.next = next;
this.direct = direct;
}
@Override
public void doYourThing() {
System.out.println("Im a Stranger");
if (direct) {
next.doYourThing();
} else {
AccessController.doPrivileged(new PrivilegedAction() {
@Override
public Object run() {
next.doYourThing();
return null;
}
});
}
}
}
第四個類TextFileDisplayer:
import java.io.CharArrayWriter;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import com.yfq.test.Doer;
public class TextFileDisplayer implementsDoer{
String fileName;
public TextFileDisplayer(String fileName){
this.fileName=fileName;
}
@Override
public void doYourThing() {
try {
FileReader fr = new FileReader(fileName);
try {
CharArrayWriter caw = new CharArrayWriter();
int c;
while((c=fr.read())!=-1){
caw.write(c);
}
System.out.println(caw.toString());
} catch (IOException e) {
e.printStackTrace();
}finally{
if(fr!=null){
try {
fr.close();
fr=null;
} catch (IOException e){
e.printStackTrace();
}
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
參考筆記http://blog.csdn.net/yfqnihao/article/details/8267669,把Friend和Stranger打包并簽名,并放到ecplise編譯目錄bin/jars下,把生成的密鑰存儲文件放在與bin同級的目錄下。(你也可以先用我上傳的源碼里的jar包,不過還是建議你動手練一練)
配置策略文件
keystore "ijvmkeys.keystore";
grant signedby "friend.keystore"{
permission java.io.FilePermission "d:/answer.txt","read";
permission java.io.FilePermission "d:/question.txt","read";
};
grant signedby"stranger.keystore" {
permission java.io.FilePermission "d:/question.txt","read";
};
grant codeBase "file:D:/workspace/MyAccessControlerStack/bin/*"{
permission java.io.FilePermission "d:/answer.txt","read";
permission java.io.FilePermission "d:/question.txt","read";
};
新建一個類,這個類里有個主函數,用于校驗類Friend,Stranger,TextFileDisplayer對于question.txt的讀取權限
import com.yfq.test.friend.Friend;
import com.yfq.test.stranger.Stranger;
public class Example2 {
public static void main(String[] args) {
TextFileDisplayer tfd=newTextFileDisplayer("d:/question.txt");
Friend friend = new Friend(tfd,true);
Stranger stranger = new Stranger(tfd,true);
stranger.doYourThing();
}
}
運行,cmd窗口輸入:
Java -classpath.;jars/friend.jar;jars/stranger.jar -Djava.security.manager -Djava.security.policy=D:/workspace/MyAccessControlerStack/src/myPolicy.txtExample2
說明:
從這里,我們并不能很直觀的發現訪問控制器的棧校驗機制,看Example2的main函數,我們知道當stranger執行doYourThing的時候,會經過這么一個過程,
Example2--------->被ApplClassLoader裝入到ProtectionDomian_Example2中---------------->執行main函數
TextFilDisplayer------->ApplClassLoader判斷當前的線程有沒有裝載類TextFilDisplayer的權限---------->裝載到ProtectionDomian_TextFileDisplayer中
Friend------->ApplClassLoader判斷當前的線程有沒有裝載類TextFilDisplayer的權限-------->裝載到ProtectionDomian_Friendr中()
Stranger------->ApplClassLoader判斷當前的線程有沒有裝載類TextFilDisplayer的權限----------->裝載到ProtectionDomian_Stranger中
Stranger的實例對象stranger執行doYourThing方法---->直接調用Friend的實力引用執行doYourThing方法----->Friend的實例引用直接調用TextFileDisplayer的doYourThing方法
輸出question.txt的文本內容。
這個過程中,AccessControler到底是在什么時候執行的,怎么執行的呢,來看下面這個圖
上面的這個圖是一個AccessControlerContext,也就是訪問控制器上下文,它大概了描述了,各個函數被調用的時候的保護域的壓棧過程,直到棧頂結束壓棧之后,它會按照先進后出的規則,AccessControler調用自己的checkPermission方法,檢驗每一層的權限(上面的保護域數組中,名為BOOTSTRAP保護域是系統保護域,它的權限是SecurityConstants.ALL_PERMISSION,這就意味著他什么都能夠做)。AccessControler的保護域數組成員則會調用自己的implies方法,ProtectionDomain的implies方法會先查看是否有配置了策略文件,如果有的話就將當前保護域傳遞給Policy這個單例,由他從配置文件中取出PermissionCollection然后再調用每個Permission檢驗它的implies方法,如果沒有設定特定的配置文件,則直接調用當前保護域中的PermissionCollecion成員的implies,再由它調用Permission的implies方法。
由于Examples2所讀取的是question.txt文本,又由于我們的策略文件中,讓Friend,Stranger,TextFileDisplayer都擁有它的讀取權限,所以順利的執行了。
為了驗證我們的猜想是正確的,我們現在修改Example2如下
import com.yfq.test.friend.Friend;
import com.yfq.test.stranger.Stranger;
public class Example {
public static void main(String[] args) {
TextFileDisplayer tfd=newTextFileDisplayer("d:/answer.txt");
Friend friend = new Friend(tfd,true);
Stranger stranger = new Stranger(tfd,true);
stranger.doYourThing();
}
}
這里我們僅僅是將question.txt換成了answer.txt,而關于這個文件我們知道Stranger是沒有讀取的權限的,下面我們來運行它看看
cmd窗口輸入java-classpath .;jars/friend.jar;jars/stranger.jar -Djava.security.manager-Djava.security.policy=D:/workspace/MyAccessControlerStack/src/myPolicy.txtExample
我們再來看AccessControlerContext的圖
前面的安全檢查都通過了,但是到了STRANGER保護域的時候,由于Stranger'沒有讀取answer.txt的權限,所以implies方法拋出了一個AccessControlException。
那么AccessControler的棧校驗機制能夠帶來什么好處呢??
答案很顯然,就好像我們第七步一樣,我們試圖讓一個沒有權限的類來調用一個具有高級權限的類別,以達到“破壞”的目的,由于棧校驗機制的存在,讓我們的這種幻想變得不容易實現,但是不容易實現并不代表不能夠實現,下面我們將來學習一個方法,這個方法叫doPrivileged(),這個方法可以幫助我們達到第七步的目的。
修改我們上面的Example類如下
import com.yfq.test.friend.Friend;
import com.yfq.test.stranger.Stranger;
public class Example3 {
public static void main(String[] args) {
TextFileDisplayer tfd=newTextFileDisplayer("d:/answer.txt");
Friend friend = new Friend(tfd,false);
Stranger stranger = new Stranger(friend,true);
stranger.doYourThing();
}
}
我們只是將friend的初始化參數做了稍微的調整,new Friend(tfd,true)改為了new Friend(tfd,false);這個調整使得friend的doYourThing方法不是直接的執行next.doYourThing()而是通過給AccessController.doPrivileged()方法傳入一個匿名內部類并重寫它的run方法,在run方法里調用了next.doYourThing()。
然后我們在cmd窗口輸入:java-classpath .;jars/friend.jar;jars/stranger.jar -Djava.security.manager-Djava.security.policy=D:/workspace/MyAccessControlerStack/src/myPolicy.txtExample3
查看輸出:既然成功由沒有權限查看answer.txt的Strange完成了查看answer.txt的操作。這是怎么回事??我們再來看剛才表示AccessControlContext的圖
由于我們在Friend中安裝了doPrivileged(),所以doPrivileged()這個方法被壓入棧而且是在Stranger的前面,doPrivileged()執行的時候會調用我的匿名內部類Friend$1并執行它的run方法,而run方法里執行完next.doYourThing之后,AccessControlContext將繼續執行判斷到doPrivileged(),它發現這是一個BootStrap的調用,那么AccessControlContext會繼續執行另外一個判斷,判斷是誰安裝了這個doPrivileged()方法,所以執行到了Freind的doYourThing(),判定它有打開answer.txt的權限,那么最后才直接把run方法的return返回出去。
就是通過這樣的方式,使得我們沒有權限的Stranger能夠“越權"操作。
但是越權還是有條件的,如第九步,我們執行”越權“方法run的方法棧幀是嵌套在Friend的doYourThing的線程棧幀中的,由于Friend有讀取answer.txt的權限,這才使得run方法有了”越獄“的機會。
我們修改一下Example3來驗證一下自己的觀點
import com.yfq.test.friend.Friend;
import com.yfq.test.stranger.Stranger;
public class Example4 {
public static void main(String[] args) {
TextFileDisplayer tfd=newTextFileDisplayer("d:/answer.txt");
Stranger stranger = new Stranger(tfd,false);
Friend friend = new Friend(stranger,true);
stranger.doYourThing();
}
}
cmd窗口輸入java-classpath .;jars/friend.jar;jars/stranger.jar -Djava.security.manager-Djava.security.policy=D:/workspace/MyAccessControlerStack/src/myPolicy.txtExample4
報出異常了,這個異常就是由于stranger$1這個內部類的方法棧幀是嵌套在stranger的doYourThing的方法棧幀中,而stranger的保護域規定了stranger這個類的對象是沒有權限讀取answer.txt這個文件,所以run這個方法也就沒辦法”越獄“。
這一節,我們學習了訪問控制器校驗保護域權限的過程,它采取的是棧的校驗機制(先進后出),而它的每個方法調用總是線程棧幀相關的,如果我們必須要”越獄“,那預約的條件要求調用doPrivileged()方法的棧幀的至少要有執行越獄操作的權限。
這一節,主要來學習jvm的基本結構,也就是概述。說是概述,內容很多,而且概念量也很大,不過關于概念方面,你不用擔心,我完全有信心,讓概念在你的腦子里變成圖形,所以只要你有耐心,仔細,認真,并發揮你的想象力,這一章之后你會充滿自信。當然,不是說看完本章,就對jvm了解了,jvm要學習的知識實在是非常的多。在你看完本節之后,后續我們還會來學jvm的細節,但是如果你在學習完本節的前提下去學習,再學習其他jvm的細節會事半功倍。
為了讓你每一個知識點都有跡可循,希望你按照我的步驟一步步繼續。
什么是Java虛擬機(你以為你知道,如果你看我下面的例子,你會發現你其實不知道)
第一步:先來寫一個類:
package test;
public class JVMTestForJava {
public static void main(String[] args) throws InterruptedException{
Thread.sleep(10000000);
}
}
第二步:cmd窗口輸入:javatest.JVMTestForJava
第三步:打開任務管理器-進程
你看到一個叫java.exe的程序沒有,是滴這個就是java的虛擬機,java xxx這個命令就是用來啟動一個java虛擬機,而main函數就是一個java應用的入口,main函數被執行時,java虛擬機就啟動了。好了ctrl+c結束你的jvm。
第四步:打開你的ecplise,右鍵runapplication,再run application一次
第五步:打開任務管理器-進程
好了,我已經圈出來了,有兩個javaw.exe,為什么會有兩個?因為我們剛才運行了兩次run application。這里我是要告訴你,一個java的application對應了一個java.exe/javaw.exe(java.exe和javaw.exe你可以把它看成java的虛擬機,一個有窗口界面一個沒有)。你運行幾個application就有幾個java.exe/javaw.exe。或者更加具體的說,你運行了幾個main函數就啟動了幾個java應用,同時也啟動了幾個java的虛擬機。
知識點1總結:
什么是java虛擬機,什么是java的虛擬機實例?java的虛擬機相當于我們的一個java類,而java虛擬機實例,相當我們new一個java類,不過java虛擬機不是通過new這個關鍵字而是通過java.exe或者javaw.exe來啟動一個虛擬機實例。
看了上面我的描述方式,你覺得如何?概念需要背嗎?如果你對我的筆記有信心,繼續看下去吧!
jvm的生命周期
基本上學習一種容器(更具體的說我們在學習servlet的時候),我們都要學習它的生命周期。那么jvm的生命周期如何,我一慣不喜歡丟概念,所以來實驗,實踐出真知,老師說過的,對不!
第一步:copy我代碼
package test;
public class JVMTestLife {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<5;i++){
try {
Thread.currentThread().sleep(i*10000);
System.out.println("睡了"+i*10+"秒");
} catch(InterruptedException e) {
System.out.println("干嘛吵醒我");
}
}
}
}).start();
for(int i=0;i<50;i++){
System.out.print(i);
}
}
}
第二步:ecplise里runapplication
第三步:打開任務管理器-進程,看到一個javaw.exe的虛擬機在跑
第四步:查看控制臺輸出,并觀察任務管理器中的javaw.exe什么時候消失
0 睡了0秒
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 1718 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 4344 45 46 47 48 49 睡了10秒
睡了20秒
睡了30秒
睡了40秒
這是我ecplise里的輸出結果,而如果你觀察控制臺和任務管理器的javaw.exe會發現,當main函數的for循環打印完的時候,程序居然沒有退出,而等到整個new Thread()里的匿名類的run方法執行結束后,javaw.exe才退出。我們知道在c++的win32編程(CreatThread()),main函數執行完了,寄宿線程也跟著退出了,在c#中如果你用線程池(ThreadPool)的話,結論也是如此,線程都跟著宿主進程的結束而結束。但是在java中貌似和我們的認知有很大的出入,這是為什么呢?
這是由于java的虛擬機種有兩種線程,一種叫叫守護線程,一種叫非守護線程,main函數就是個非守護線程,虛擬機的gc就是一個守護線程。java的虛擬機中,只要有任何非守護線程還沒有結束,java虛擬機的實例都不會退出,所以即使main函數這個非守護線程退出,但是由于在main函數中啟動的匿名線程也是非守護線程,它還沒有結束,所以jvm沒辦法退出(有沒有想干壞事的感覺??)。
知識點2總結:java虛擬機的生命周期,當一個java應用main函數啟動時虛擬機也同時被啟動,而只有當在虛擬機實例中的所有非守護進程都結束時,java虛擬機實例才結束生命。
java虛擬機的體系結構(無奈,我懷著悲痛心情告訴你,我們必須來一些概念,別急,咱有圖)
看到這個圖沒,名詞不是普通滴多,先來看看哪些名詞我們之前是說過的,執行引擎(筆記一),類裝載器(筆記二),java棧(筆記十一)。
在了解jvm的結構之前,我們有必要先來了解一下操作系統的內存基本結構,這段可不能跳過,它會有助于消化上面的那個圖哦!好先來看圖
操作系統內存布局:
那么jvm在操作系統中如何表示的呢?
操作系統中的jvm
為什么jvm的內存是分布在操作系統的堆中呢??因為操作系統的棧是操作系統管理的,它隨時會被回收,所以如果jvm放在棧中,那java的一個null對象就很難確定會被誰回收了,那gc的存在就一點意義都莫有了,而要對棧做到自動釋放也是jvm需要考慮的,所以放在堆中就最合適不過了。
操作系統+jvm的內存簡單布局
從上圖中,你有沒有發現什么規律,jvm的內存結構居然和操作系統的結構驚人的一致,你能不能給他們對號入座?還不能,沒關系,再來看一個圖,我幫你對號入座。看我下面紅色的標注
從這個圖,你應該不難發現,原來jvm的設計的模型其實就是操作系統的模型,基于操作系統的角度,jvm就是個該死的java.exe/javaw.exe,也就是一個應用,而基于class文件來說,jvm就是個操作系統,而jvm的方法區,也就相當于操作系統的硬盤區,所以你知道我為什么喜歡叫他permanent區嗎,因為這個單詞是永久的意思,也就是永久區,我們的磁盤就是不斷電的永久區嘛,是一樣的意思啊,多好對應啊。而java棧和操作系統棧是一致的,無論是生長方向還是管理的方式,至于堆嘛,雖然概念上一致目標也一致,分配內存的方式也一直(new,或者malloc等等),但是由于他們的管理方式不同,jvm是gc回收,而操作系統是程序員手動釋放,所以在算法上有很多的差異,gc的回收算法,估計是jvm里面的經典啊,后面我們也會一點點的學習的,不要著急。
有沒有突然自信的感覺?如果你對我的文章有自信,我們再繼續,還是以圖解的方式,我還是那一句,對于概念我絕對有信心讓它在你腦子里根深蒂固。
看下面的圖。
將這個圖和上面的圖對比多了什么?沒錯,多了一個pc寄存器,我為什么要畫出來,主要是要告訴你,所謂pc寄存器,無論是在虛擬機中還是在我們虛擬機所寄宿的操作系統中功能目的是一致的,計算機上的pc寄存器是計算機上的硬件,本來就是屬于計算機,(這一點對于學過匯編的同學應該很容易理解,有很多的寄存器eax,esp之類的32位寄存器,jvm里的寄存器就相當于匯編里的esp寄存器),計算機用pc寄存器來存放“偽指令”或地址,而相對于虛擬機,pc寄存器它表現為一塊內存(一個字長,虛擬機要求字長最小為32位),虛擬機的pc寄存器的功能也是存放偽指令,更確切的說存放的是將要執行指令的地址,它甚至可以是操作系統指令的本地地址,當虛擬機正在執行的方法是一個本地方法的時候,jvm的pc寄存器存儲的值是undefined,所以你現在應該很明確的知道,虛擬機的pc寄存器是用于存放下一條將要執行的指令的地址(字節碼流)。
再對上面的圖擴展,這一次,我們會稍微的深入一點,放心啦,不會很深入,我們的目標是淺顯易懂,好學易記嘛!看下面的圖。
多了什么?沒錯多了一個classLoader,其實這個圖是要告訴你,當一個classLoder啟動的時候,classLoader的生存地點在jvm中的堆,然后它會去主機硬盤上將A.class裝載到jvm的方法區,方法區中的這個字節文件會被虛擬機拿來new A字節碼(),然后在堆內存生成了一個A字節碼的對象,然后A字節碼這個內存文件有兩個引用一個指向A的class對象,一個指向加載自己的classLoader,如下圖。
那么方法區中的字節碼內存塊,除了記錄一個class自己的class對象引用和一個加載自己的ClassLoader引用之外,還記錄了什么信息呢??我們還是看圖,然后我會講給你聽,聽過一遍之后一輩子都不會忘記。
你仔細將這個字節碼和我們的類對應,是不是和一個基本的java類驚人的一致?下面你看我貼出的一個類的基本結構。
package test;
import java.io.Serializable;
public final class ClassStruct extendsObject implements Serializable {//1.類信息
//2.對象字段信息
private String name;
private int id;
//4.常量池
public final int CONST_INT=0;
public final String CONST_STR="CONST_STR";
//5.類變量區
public static String static_str="static_str";
//3.方法信息
public static final String getStatic_str()throws Exception{
return ClassStruct.static_str;
}
}
你將上面的代碼注解和上面的那個字節碼碼內存塊按標號對應一下,有沒有發現,其實內存的字節碼塊就是完整的把你整個類裝到了內存而已。
所以各個信息段記錄的信息可以從我們的類結構中得到,不需要你硬背,你認真的看過我下面的描述一遍估計就不可能會忘記了:
1.類信息:修飾符(publicfinal)
是類還是接口(class,interface)
類的全限定名(Test/ClassStruct.class)
直接父類的全限定名(java/lang/Object.class)
直接父接口的權限定名數組(java/io/Serializable)
也就是 public final class ClassStruct extendsObject implements Serializable這段描述的信息提取
2.字段信息:修飾符(pirvate)
字段類型(java/lang/String.class)
字段名(name)
也就是類似private String name;這段描述信息的提取
3.方法信息:修飾符(public static final)
方法返回值(java/lang/String.class)
方法名(getStatic_str)
參數需要用到的局部變量的大小還有操作數棧大小(操作數棧我們后面會講)
方法體的字節碼(就是花括號里的內容)
異常表(throws Exception)
也就是對方法public static final StringgetStatic_str ()throws Exception的字節碼的提取
4.常量池:
4.1.直接常量:
1.1CONSTANT_INGETER_INFO整型直接常量池 public final int CONST_INT=0;
1.2CONSTANT_String_info字符串直接常量池 public final String CONST_STR="CONST_STR";
1.3CONSTANT_DOUBLE_INFO浮點型直接常量池
等等各種基本數據類型基礎常量池(待會我們會反編譯一個類,來查看它的常量池等。)
4.2.方法名、方法描述符、類名、字段名,字段描述符的符號引用
也就是所以編譯器能夠被確定,能夠被快速查找的內容都存放在這里,它像數組一樣通過索引訪問,就是專門用來做查找的。
編譯時就能確定數值的常量類型都會復制它的所有常量到自己的常量池中,或者嵌入到它的字節碼流中。作為常量池或者字節碼流的一部分,編譯時常量保存在方法區中,就和一般的類變量一樣。但是當一般的類變量作為他們的類型的一部分數據而保存的時候,編譯時常量作為使用它們的類型的一部分而保存
5.類變量:
就是靜態字段( public static Stringstatic_str="static_str";)
虛擬機在使用某個類之前,必須在方法區為這些類變量分配空間。
6.一個到classLoader的引用,通過this.getClass().getClassLoader()來取得為什么要先經過class呢?思考一下,然后看第七點的解釋,再回來思考
7.一個到class對象的引用,這個對象存儲了所有這個字節碼內存塊的相關信息。所以你能夠看到的區域,比如:類信息,你可以通過this.getClass().getName()取得
所有的方法信息,可以通過this.getClass().getDeclaredMethods(),字段信息可以通過this.getClass().getDeclaredFields(),等等,所以在字節碼中你想得到的,調用的,通過class這個引用基本都能夠幫你完成。因為他就是字節碼在內存塊在堆中的一個對象
8.方法表,如果學習c++的人應該都知道c++的對象內存模型有一個叫虛表的東西,java本來的名字就叫c++- -,它的方法表其實說白了就是c++的虛表,它的內容就是這個類的所有實例可能被調用的所有實例方法的直接引用。也是為了動態綁定的快速定位而做的一個類似緩存的查找表,它以數組的形式存在于內存中。不過這個表不是必須存在的,取決于虛擬機的設計者,以及運行虛擬機的機器是否有足夠的內存
大哭好了,還剩這么多沒講過。不過不要急,我一向提倡,學到哪里講到哪里,看到哪里。所以沒有學到的概念,讓他隨風去。
但是我還是會來串一下思路滴:
首先,當一個程序啟動之前,它的class會被類裝載器裝入方法區(不好聽,其實這個區我喜歡叫做Permanent區),執行引擎讀取方法區的字節碼自適應解析,邊解析就邊運行(其中一種方式),然后pc寄存器指向了main函數所在位置,虛擬機開始為main函數在java棧中預留一個棧幀(每個方法都對應一個棧幀),然后開始跑main函數,main函數里的代碼被執行引擎映射成本地操作系統里相應的實現,然后調用本地方法接口,本地方法運行的時候,操縱系統會為本地方法分配本地方法棧,用來儲存一些臨時變量,然后運行本地方法,調用操作系統APIi等等。
好吧,你聽暈了,我知道,先記住這段話的位置,等某年某月我提醒你回來看,你就煥然大悟了,現在你只需要走馬觀花咯!!!
好了,這一節的內容實在夠多了,所以我打算把它拆解一下,剩下的內容放到下一節,下一節我們會來學習虛擬機的堆棧,和堆。
大神總結的目錄:http://blog.csdn.net/yfqnihao/article/details/8257491(轉載),僅供個人學習,如有抄襲請包容.....
|
新聞熱點
疑難解答