首页 > 编程 > Java > 正文

java的JVM虚拟机相关知识,简单易懂。

2019-11-06 06:07:40
字体:
来源:转载
供稿:网友

一、    java class执行方式

1)  概要

Java的class只在需要的时候才内转载入内存,并由java虚拟机的执行引擎来执行,而执行引擎从总的来说主要的执行方式分为四种,

第一种,一次性解释代码,也就是当字节码转载到内存后,每次需要都会重新的解析一次,

第二种,即时解析,也就是转载到内存的字节码会被解析成本地机器码,并缓存起来以提高重用性,但是比较耗内存,

第三种,自适应优化解析,即将java将使用最频繁的代码编译成本地机器码,而使用不频繁的则保持字节码不变,一个自适应的优化器可以使得java虚拟机在80%-90%的时间里执行优化过的本地代码,而只需要执行10%-20%对性能有影响的代码。

第四种,一种能够利用本地方法直接解析java字节码的芯片。Java TCP网络基础

二、    类装载器的体系结构

1)  java沙箱

在了解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有一个默认的安全策略管理器,用户可以使用默认的安全策略管理器也可以在它之上进行扩展。

2)  java类装载器的体系结构

java的类装载器从三方面对java的沙箱起作用:

3)  1.它防止恶意的代码区干扰善意的代码

怎么理解这句话,不同的类装载器装入同样的类的时候会产生一个唯一的命名空间,java虚拟机维护着这些命名空间,同一类,一个命名空间只能装载一次,也只会装载一次,不同命名空间之间的类就如同各自有一个防护罩,感觉不到彼此的存在,如下图3-1所示

4)  2.它守护了被信任的类库边界

这里有两个需要理解的概念,一,双亲委托模式,二运行时包,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虚拟机只允许由同一个类装载器装载到同一包中的类型互相访问,而由同一类装载器装载,属于同一个包的,多个类型的集合就是我们所指的运行时包了。

5)  3.将代码归入某类(保护域),该类确定了代码能够执行那些操作

除了1.屏蔽不同的命名空间,2.保护信任类库的边界外,类装载器的第三个重要的作用就是保护域,类装载器必须把代码放入到保护域中以限定这些代码运行时能够执行的操作的权限,这也如我上面讲的,像一个监狱一样,不让它在监狱意外的范围活动。

三、    Class文件检验器

1)  概要

前面的学习我们知道了class文件被类装载器所装载,但是在装载class文件之前或之后,class文件实际上还需要被校验,这就是今天的学习主题,class文件校验器。

class文件校验器,保证class文件内容有正确的内部结构,Java虚拟机的class文件检验器在字节码执行之前对文件进行校验,而不是在执行中进行校验

class文件校验器要进行四趟独立的扫描来完成校验工作

2)  第一趟

在装载字节序列的时候进行,这个是校验class文件的结构的合法性,比如你使用windowns下的copy命令去合并一个.class文件和一个jpg文件的时候,在装载这个class文件的时候jvm会发现这个class文件被删改过,文件的长度也不正确,而抛出异常!

所以这次校验是发生在二进制数据上,

3)  第二趟

扫描发生在方法区中,主要对于,语义,词法和语法的分析,也就是检查这个类是否能够顺利的编译!

4)  第三趟

字节码校验

在这一趟的校验中涉及两个比较不好理解的概念,第一个是字节码流,第二个是栈帧.

执行字节码时,一次执行操作码,java虚拟机内构成了执行线程,而每个线程会有自己的java栈就是我们说的栈帧。每一个方法都有一个栈帧。

如果学过汇编的人理解这两个概念会容易一点

字节码流=操作码+操作数,在这里可以看做汇编里的伪指令+操作数,因为这里的操作码实际上就是给jvm识别的“汇编伪指令”,而操作数的概念和汇编里的除了数据类型,并没有多大的差异

重点来看一下栈帧,栈帧其实也很好理解,栈帧里有局部变量栈和操作数栈,这两块内存就是放数据的时机不同,操作数栈就是用来存放字节码指令执行的中间结果,结果或操作数,而局部变量区,就是用来存局部变量形参等,这个很好理解

这个字节码的校验过程校验的就是字节码流的合法过程,也就是校验操作数+操作码的合法性。

而java的class文件编码我们之所以称之为字节码,是因为每调条操作指令都只占一个字节,除了两个例外情况,所有的操作码和他们的操作数按字节对齐,这使得字节流在传输的时候跟小,更有优势,这两个例外是这样一些操作码,在操作码和他们的操作数之间会天上一至三个字节,以便操作数都按字节对齐。

下面是一个图,描述了栈帧的结构

5)  第四趟

符号引用的校验

由于大部分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文件的内部结构,保证能够被正常安全的编译

第二趟和第三趟在连接的过程中进行,这两趟基本上是语法校验,词法校验

第四趟是解析符号引用和直接引用时进行的,这次校验确认被引用的类,字段以及方法确实存在

四、    安全管理器

1)  概要

前面已经简述了Java的安全模型的两个组成部分(类装载器,class文件校验器),接下来学习的是java安全模型的另外一个重要组成部分安全管理器。

安全管理器是一个单独的对象,在java虚拟机中,它在访问控制-对于外部资源的访问控制-起到中枢作用

2)  ClassLoader构造

如果光看概念可能并不能很好的理解,或者说比较抽象,下面是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); 

}

3)  安全管理器security

这里又调用了另外一个方法,从方法名字上,就可以猜到这个方法是用来校验权限的,校验是否有创建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就可以了。

4)  安装前后的demo区别

下面我们用熟悉的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虚拟机中,它在访问控制-对于外部资源的访问控制-起到中枢作用

五、    实践写自己的类装载器

1)  概要

前面第三和第四节我们一直在强调一句话,类装载器和安全管理器是可以被动态扩展的,或者说,他们是可以由用户自己定制的,今天我们就是动手试试,怎么做这部分的实践,当然,在阅读本篇之前,至少要阅读过笔记三。

下面我们先来动态扩展一个类装载器,当然这只是一个比较小的demo,旨在让大家有个比较形象的概念。

2)  第一步

首先定义自己的类装载器,从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; 

    }

3)  第二步

定义一个类,专门用于被装载,这里我们定义了一个静态代码块,待会用到它

public class TestBeLoader { 

   static{ 

       System.out.println("TestBeLoader init"); 

   } 

   public void sayHello(){ 

       System.out.println("hello"); 

   } 

}

4)  第三步

定义一个有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(); 

       } 

   } 

}

5)  第四步

查看运行结果

sun.misc.Launcher$AppClassLoader@19821f

sun.misc.Launcher$AppClassLoader@19821f

sun.misc.Launcher$AppClassLoader@19821f

TestBeLoader init

6)  说明:

第一个输出:装载TestClassLoaderDemo的类是AppClassLoder

第二个输出:装载myClassLoader的装载器也是AppClassLoader,这里也验证了我们笔记三讲的,在同个线程中,动态连接模式会运用当前线程的类加载器来加载所需的class文件,因为第一个和第二个输出是同一个对象的对象名

第三个输出:是TestBeLoader的类加载器,这个输出验证了,双亲委托模式下的动态连接模式,由于myClassLoader是由AppClassLoader装载的,所以它会委托自己的parent来装载com.yfq.test.TestBeLoader这个类,加载成功所以就不再调用自己的findClass方法,这个我们在笔记三有做过简要的讨论。

第四个输出:如果我们将TestBeLoadertest=(TestBeLoader)cls2.newInstance();这句话注掉,则不会有第四个输出,为什么?

类的装载大致分为三步,装载,连接,初始化。而初始化这一步,是在我们第一次创建对象的时候才进行初始化分配内存,这一点需要注意,并不是class被load内存后就立刻初始化。

六、    实践写自己的安全管理器

1)  概要

安全管理器SecurityManager里设计的内容实在是非常的庞大,它的核心方法就是checkPerssiom这个方法里又调用AccessController的checkPerssiom方法,访问控制器AccessController的栈检查机制又遍历整个PerssiomCollection来判断具体拥有什么权限一旦发现栈中一个权限不允许的时候抛出异常否则简单的返回,这个过程实际上比我的描述要复杂得多,这里我只是简单的一句带过,因为这里涉及到很多比较后面的知识点。

下面来尝试一下写一个非常简单的demo,旨在让你有一个比较形象的思维,不会在概念上打转。

2)  第一步

定义一个类继承自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("你没有读取的本文件的权限");   

    }

3)  第二步

定义一个有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(); 

       } 

 

   } 

}

4)  第三步

运行代码查看控制台输出

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安装过默认的安全管理器,有印象不?

5)  拓展内容

好了,我们的安全管理器是怎么被执行的呢?如果你有兴趣可以继续往下看一下,也可以跳过,这里只是简单的介绍一下,也是本人习惯的学习思路

直接跳到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就到这里。意在浅显易懂!

申明:文章的部分内容有参照网上的其他作者的内容,这里只用来学习交流!

七、    jar包的代码认证和签名

1)  概要

如果你循序渐进的看到这里,那么说明你的毅力提高了,jvm的很多东西都是比较抽像的,如果不找相对应的代码来辅助理解,其实很难有个比较形象的思维,前面我努力的尝试通过将概念投射到代码的方式去讲解jvm的各个细节,希望你能够试着自己也去找到对应的代码段,然后试着读一读。一开始可能没有那么容易,但是没有一件事情,一开始就是容易的。

2)  代码认证与签名

终于到了这一节,这一节,其实相对于笔记二,笔记三和笔记四,是相对比较容易的,即使你对密码编码学一窍不通也不妨碍你学习,我们不会涉及到太多的实现,而主要从应用着手,旨在浅显易懂,触类旁通。在下一节中,我们会来尝试做一次签名,前提是你看完这一节

笔记3的时候我们曾经提到class文件的校验器,记得它分为几趟不,四趟,而jar包的代码签名认证和class检验的第一趟是有联系的。

class文件校验器的第一趟会对jar文件的结构,长度等进行校验,其中也包括对jar的签名和认证进行校验。

3)  那么什么是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的过程,如果你看到这个图完全明白,那前面那段废话就直接跳过吧!

4)  接下来还有一个概念需要你理解----认证

先不管什么是认证,先来了解一下密码学的一点小知识

前面我说过,看这篇文章是不需要你有密码学的知识的,是的,我骗你,至少基本的概念还是要理解过的。如果你完全不懂,不要慌,我举个简单的例子来帮你简单的理解一下一两个基本的概念。

第一个概念对称加密,什么是对称加密?假设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包进行签名)

八、    实践对jar包的代码签名

1)  概要

这一节,以实践为主,在跟着我做相应的操作之前,我希望你已经能够理解笔记七所提到的概念,至少你应该对于笔记七的那个大图有所了解。

好了!对于习惯用ecplise的朋友今天不得不逼迫你把jdk的环境搭建出来!下面让我们动手来实践一下对jar进行签名吧!

2)  第一步

首先配置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,看看命令是否生效,如果配置成功执行第二步。

3)  第二步

来写几个简单的类,简单的才是大家的。你完全可以直接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(工具是拿来用的,不要浪费这些强大的功能!)

4)  第三步

打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签名的实验已经完毕!!!!!

5)  总结

查看上面步骤四截图,我们来验证一下在笔记七里说过的话。

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包签名的时候也从这篇文章中收益匪浅,希望它对你有帮助。

九、    策略文件

1)  概要

什么是Java的策略,什么又是策略文件。

今天我换一下笔记的方式,不是直接讲概念,而是先来做一个小例子,相信你做完这个例子之后再看我对例子的讲解,你对策略,策略文件,会豁然开朗的感觉。

例子很简单,简单的才是大家的,下面跟着我(你完全可以copy我的代码)。

2)  第一步

定义一个简单类。

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(); 

           } 

       }

3)  第二步

定义一个简单的策略文件,我们放到工程的类路径下(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这个文件只分配读的权限。

4)  第三步

运行,在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下,如果你有兴趣可以自己再多摸索)。

5)  第四步

查看输出

这里报出了异常,提示本应用对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对象来帮助他们决定,在把一段代码导入虚拟机时应该给予什么权限。

如果你之前有稍微听过策略这个概念,希望看完本文有给你豁然开朗的感觉

十、    策略和保护域

1)  概要

前面一节,我们做了一个简单的实验,来说明什么是策略文件,在文章的最后,也顺带的讲了一下什么是策略,还有策略的作用。

为了引出另外一个很重要的概念ProtectionDomain(保护域),所以我们还是要先来回顾一下什么是策略。

首先,什么是策略,今天的东西纯粹是比较概念的。当然,如果你读过笔记九,今天的东西,就真的是soso

2)  策略与策略文件:

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内存的时候会将它放置到一个保护域中,是滴今天我就来说说什么是保护域。

3)  什么是保护域

当类装载器将类型装入Java虚拟机时,它们将为每个类型指派一个保护域。保护域定义了授予一段特定代码的所有权限。(一个保护域对应策略文件中的一个或多个Grant子句。)装载入Java虚拟机的每一个类型都属于一个且仅属于一个保护域。

类装载器知道它装载的所有类或接口的代码库和签名者。它利用这些信息来创建一个CodeSource对象。它将这个CodeSource对象传递个当前Policy对象的getPermissions()方法,得到这个抽象类java.security.PermissionCollection的子类实例。这个PermissinCollection包含了到所有Permission对象的引用(这些Permission对象由当前策略授予指定代码来源)。利用它创建的CodeSource和它冲Policy对象得到的PermissionCollection,它可以实例化一个新的ProtectDomain对象。它通过将合适的ProtectionDomain对象传递给defineClass()方法,来将这段代码放到一个保护域中

如果你对上面这段话理解不了,看下面这个图

好了看完上面的这整个过程之后你是否已经理解什么是保护域了。

4)  总结

下面我们再整理一下今天的内容,概念有点多,一个一个的来。

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,如果你想知道访问控制器是干什么的,做什么工作,怎么和安全管理进行合作,那么请你阅读下一节。

十一、        访问控制器

1)  概要

这一节,我们要学习的是访问控制器,在阅读本节之前,如果没有前面几节的基础,对你来说可能会比较困难!

知识回顾:

我们先来回顾一下前几节的内容,在笔记三的时候我们学了类装载器,它主要的功能就是装载类,在装载的前后,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里的源码,看看他和安全管理是怎么配合工作的。

2)  那么什么是访问控制器?

类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稍微会复杂一点

3)  步骤一

试着实现自己的安全管理器,实验是否成功,以下主要分三步来完成

第一步:实现一个自己的类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) 

好了,到这里说明我们自己的安全管理器安装上去了。上面的异常正好是我们期望见到的。

4)  步骤二

我们来实现一个自己的类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());前的注释符号去掉再运行

好了,终于完整的按照我们期望执行了。

5)  小总结

上面的步骤一和步骤二,忽略整个调试的过程的话,其实思路很清晰了:

1.注册我们的安全管理器

2.实例化一个我们自己的类,这个类调用安全管理器的checkRead方法校验自己有没有相应的权限

3.MySecurityManager的checkRead方法由于只跑出一个异常,所以直接退出了程序。

这个过程其实就是我们的每个类调用安全管理器的过程,是一个比较简单的模拟,好好的玩味一下,然后开始我们的步骤三

到了这里,我们只是做了步骤一和步骤二,是不是一个很艰苦的过程?后面不难,真的不难,虽然我一直这么说,简单的才是大家的,但是难的才是自己的,哈哈哈。

6)  步骤三

实现我们的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(); 

       }

   } 

}

运行:

7)  总结

到这里,我们基本上已经走顺了安全管理器MySecurityManager和访问控制器MyAccessControler是怎么配合工作的了,它大致是这么一个过程,需要权限控制的类会new一个Permission的子类对象(我们的例子里采用MyFileInputStream)然后传递给我们的安全控制器里(我们的例子里自己定义了一个MySecurityManager)的checkPermission方法,而这个方法什么也不干就是调用AccessControler的静态方法checkPermission,我们自己的MyAccessControler里的checkPermission方法我们只是简单的调用了Permission的implies方法,而,其实这也正是整个java虚拟机安全校验的总体脉络,但是这里我们还有一个疑问,AccessControler它到底是怎么和我们笔记九和笔记十的策略和策略文件配合工作的呢???请看下一节,访问控制器的栈校验机制

十二、        访问控制器的栈校验机制

1)  概要

这一节,我们会简单的描述一下jvm访问控制器的栈校验机制。

这节课,我们还是以实践为主,什么是栈校验机制,讲一百遍不如你自己实际的代码一下然后验证一下,下面我们下把环境搭起来。

2)  第一步

配置系统环境。

path=%JAVA_HOME%/bin 

JAVA_HOME=C:/Java/jdk1.6.0_01 

CLASSPATH=.;%JAVA_HOME%/lib/dt.jar;%JAVA_HOME%/lib/tools.jar     

3)  第二步

配置一个策略文件的运行环境。

第一个类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(); 

       }

    }

}

4)  第三步

参考笔记http://blog.csdn.net/yfqnihao/article/details/8267669,把Friend和Stranger打包并签名,并放到ecplise编译目录bin/jars下,把生成的密钥存储文件放在与bin同级的目录下。(你也可以先用我上传的源码里的jar包,不过还是建议你动手练一练)

5)  第四步

配置策略文件

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";  

};  

6)  第五步

新建一个类,这个类里有个主函数,用于校验类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(); 

   } 

}

7)  第六步

运行,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都拥有它的读取权限,所以顺利的执行了。

8)  第七步

为了验证我们的猜想是正确的,我们现在修改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是没有读取的权限的,下面我们来运行它看看

9)  第八步

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(),这个方法可以帮助我们达到第七步的目的。

10)      第九步

修改我们上面的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()。

11)      第十步

然后我们在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方法有了”越狱“的机会。

12)      第十一步

我们修改一下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(); 

   } 

}

13)      第十二步

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这个方法也就没办法”越狱“。

14)      总结:

这一节,我们学习了访问控制器校验保护域权限的过程,它采取的是栈的校验机制(先进后出),而它的每个方法调用总是线程栈帧相关的,如果我们必须要”越狱“,那预约的条件要求调用doPrivileged()方法的栈帧的至少要有执行越狱操作的权限。

十三、        jvm基本结构

1)  概要

这一节,主要来学习jvm的基本结构,也就是概述。说是概述,内容很多,而且概念量也很大,不过关于概念方面,你不用担心,我完全有信心,让概念在你的脑子里变成图形,所以只要你有耐心,仔细,认真,并发挥你的想象力,这一章之后你会充满自信。当然,不是说看完本章,就对jvm了解了,jvm要学习的知识实在是非常的多。在你看完本节之后,后续我们还会来学jvm的细节,但是如果你在学习完本节的前提下去学习,再学习其他jvm的细节会事半功倍。

为了让你每一个知识点都有迹可循,希望你按照我的步骤一步步继续。

2)  知识点1

什么是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来启动一个虚拟机实例。

看了上面我的描述方式,你觉得如何?概念需要背吗?如果你对我的笔记有信心,继续看下去吧!

3)  知识点2

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虚拟机实例才结束生命。

4)  知识点三

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++的虚表,它的内容就是这个类的所有实例可能被调用的所有实例方法的直接引用。也是为了动态绑定的快速定位而做的一个类似缓存的查找表,它以数组的形式存在于内存中。不过这个表不是必须存在的,取决于虚拟机的设计者,以及运行虚拟机的机器是否有足够的内存

大哭好了,还剩这么多没讲过。不过不要急,我一向提倡,学到哪里讲到哪里,看到哪里。所以没有学到的概念,让他随风去。

5)  总结

但是我还是会来串一下思路滴:

首先,当一个程序启动之前,它的class会被类装载器装入方法区(不好听,其实这个区我喜欢叫做Permanent区),执行引擎读取方法区的字节码自适应解析,边解析就边运行(其中一种方式),然后pc寄存器指向了main函数所在位置,虚拟机开始为main函数在java栈中预留一个栈帧(每个方法都对应一个栈帧),然后开始跑main函数,main函数里的代码被执行引擎映射成本地操作系统里相应的实现,然后调用本地方法接口,本地方法运行的时候,操纵系统会为本地方法分配本地方法栈,用来储存一些临时变量,然后运行本地方法,调用操作系统APIi等等。        

好吧,你听晕了,我知道,先记住这段话的位置,等某年某月我提醒你回来看,你就焕然大悟了,现在你只需要走马观花咯!!!

好了,这一节的内容实在够多了,所以我打算把它拆解一下,剩下的内容放到下一节,下一节我们会来学习虚拟机的堆栈,和堆。

大神总结的目录:http://blog.csdn.net/yfqnihao/article/details/8257491(转载),仅供个人学习,如有抄袭请包容.....


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表