java jvm学习笔记十一(访问控制器)

       欢迎装载请说明出处: http://blog.csdn.net/yfqnihao/article/details/8271665

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

                  本节实验源码下载:http://download.csdn.net/detail/yfqnihao/4863854

                  知识回顾:

                  我们先来回顾一下前几节的内容,在笔记三的时候我们学了类装载器,它主要的功能就是装载类,在装载的前后,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 = new FilePermission("d:/tmp/test.txt",SecurityConstants.FILE_READ_ACTION);
        Permission perAll = new FilePermission("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吧少年,要的是你知识的储备,不是要你把代码背下来),       

package com.yfq.test;

public class MySecurityManager extends SecurityManager {

    @Override
    public void checkRead(String file) {
        //super.checkRead(file, context);
        throw new SecurityException("你没有的权限");  
    }
    
}

 


 

                   第二步:实现一个简单的类,主要用来测试我们自己定义的安全管理器起作用了没有,我们这里借助了FileInputStream,因为FileInputStream会调用安全管理器去校验权限(我们在笔记六已经详细的讲解过),所以用FileInputStream测试我们自己的安全管理器非常的适合。    

package com.yfq.test;

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 = new FileInputStream("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吧少年,不要自己狂敲)

package com.yfq.test;

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(File file)这个构造函数通过System.getSecurityManager();取出当前的SecurityManager,然后调用它的checkRead方法。是滴,这个其实是FileInputStream源码里的逻辑,我只是把一些有妨碍我们理解的代码去掉了而已。

                    第二步,修改步骤一里的TestMySecurityManager里的main用自己的类替换FileInputStream函数如下

 

package com.yfq.test;

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/bin com.yfq.test.TestMySecurityManager。

                        查看控制台输出   
                

报错的提示变了,它提示MyFileInputStream这个类找不到,但是它命名和com.yfq.test.TestMySecurityManager在同个编译目录下,为什么?

好吧,这里我就不绕弯子了,我们再修改com.yfq.test.TestMySecurityManager,将设置自己的安全管理器的那行先简单地注释掉如下

package com.yfq.test;

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/bin com.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:\Program Files\Java\jdk1.6.0_12\lib\tools.jar";"C:\Program Files\Java\jdk1.6.0_12\lib\dt.jar";"D:/workspace/MySecurityManager/bin";. com.yfq.test.TestMySecurityManager

                                                   第四种方法:java -classpath  "C:\Program Files\Java\jdk1.6.0_12\lib\tools.jar";"C:\Program Files\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那样去继承它的父类,所以我们就自己定义一个类,我要你做的还是copy我的代码。          

<p>package com.yfq.test;</p><p>import java.security.AccessControlException;
import java.security.Permission;</p><p>
public class MyAccessControler {
&nbsp;&nbsp; public static void checkPermission(Permission perm)
&nbsp;&nbsp; throws AccessControlException 
&nbsp; {
&nbsp;&nbsp;&nbsp;throw new SecurityException("你没有的权限");&nbsp; 
&nbsp; }
}
</p>

 


                              第二步,修改MySecurityManage,重写父类SecurityManager的checkRead方法和checkPermission方法如下 

package com.yfq.test;

import java.io.FilePermission;
import java.security.Permission;

import sun.security.util.SecurityConstants;

public class MySecurityManager extends SecurityManager {
    @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:\Program Files\Java\jdk1.6.0_12\lib\tools.jar";"C:\Program Files\Java\jdk1.6.0_12\lib\dt.jar";"D:/workspace/MySecurityManager/bin";. com.yfq.test.TestMySecurityManager
                               

                              恭喜你,哈,报错了,而且是一个很不常见的错误,类循环加载错误,你一定很好奇,怎么会循环加载错误,这个问题很多人定义自己的安全管理器的时候都遇到过,但是它是怎么产生的?下面我们来改一行代码,再看它的错误信息,你就知道它是怎么产生的了,接着第四步。

                                第四步,修改上面的MySecurityManage类的checkPermission方法。如下

package com.yfq.test;

import java.io.FilePermission;
import java.security.Permission;

import sun.security.util.SecurityConstants;

public class MySecurityManager extends SecurityManager {
    @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:\Program Files\Java\jdk1.6.0_12\lib\dt.jar";"D:/workspace/MySecurityManager/bin";. com.yfq.test.TestMySecurityManager

 

报错很长无止境,这里我们截取了重复的报错内容出来,看到Exception in thread "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)。

不信的话,我们来做个试验,复制下面的代码,跑一下

package com.yfq.test;

import java.security.Permission;

public class Bug {
    public static class A {}

    public static void main(String[] args) throws Exception {
    System.out.println("Setting Security Manager");
    System.setSecurityManager(new SecurityManager() {
    public void checkPermission(Permission p) {
    new A();
    }
    });
    System.out.println("Post set.");
    }
    }

 

 

运行一下

好吧,它就是个可恶的bug,每次new它都要来链接一次,这样就出现循环链接了,那么我们如何来解决这个bug呢?(如你把上面的式样程序 new A()改成new Bug()思考一下,为什么这个我们说的“bug”为什么会不见了)

                               第五步,再修改上面的MySecurityManage类的checkPermission方法。如下

package com.yfq.test;

import java.io.FilePermission;
import java.security.Permission;

import sun.security.util.SecurityConstants;

public class MySecurityManager extends SecurityManager {
    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:\Program Files\Java\jdk1.6.0_12\lib\dt.jar";"D:/workspace/MySecurityManager/bin";. com.yfq.test.TestMySecurityManager

亲切的画面有木有!!!!!
                       第六步,对MyAccessControler中的checkPermission做简单的实现,copy我的代码

package com.yfq.test;

import java.io.FilePermission;
import java.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 = new FilePermission("d:/tmp/*",SecurityConstants.FILE_READ_ACTION);
            if(perAll.implies(perm)){
                System.out.println("你可以读取这个文件哦!");
            }else{
                throw new AccessControlException("你没有读取这个文件的权限");
            }
     }
}

 

修改TestMySecurityManager中的main如下

package com.yfq.test;


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它到底是怎么和我们笔记九和笔记十的策略和策略文件配合工作的呢???请看下一节,访问控制器的栈校验机制

posted @ 2015-11-18 11:10  走在大牛的路上  阅读(742)  评论(0编辑  收藏  举报