实例分析JVM安全体系:双亲委派、命名空间、保护域、策略

在了解双亲委派模型之前,先了解一下类加载器的概念:

类加载器的作用就是将真实的class文件根据位置将该Java类的字节码装入内存,并生成对应的Class对象。用户可以通过继承ClassLoader和重写findClass方法来定义自己的类加载器进行加载,系统类加载器按照层次,分为: 
(1).启动类加载器(Bootstrap ClassLoader):将加载 /JAVAHOME/lib以及为-Xbootclasspath所指定的目录下的类库,是核心Java API的class文件,用于启动Java虚拟机 
(2).扩展类加载器(Extension ClassLoader):将加载/JAVAHOME/lib/ext以及为java.ext.dirs所指定的目录下的类库 
(3).应用程序类加载器(Application/System ClassLoader):将加载ClassPath下所指定的类库,或者称为类路径加载器

1.双亲委派

  类的加载将使用双亲委派的方式,注意这里的双亲关系并非通过继承来实现,而是加载器之间指定或默认的委托加载关系,可以看到在/java/lang/ClassLoader.java中,通过ClassLoader的构造方法显式指定了其父加载器,而若没有指定父加载器,那么将会把系统类加载器AppClassLoader作为默认的父加载器

private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        //...
}

protected ClassLoader() {
        //getSystemClassLoader()
        this(checkCreateClassLoader(), getSystemClassLoader());
}

  加载器对类的加载调用loadClass()方法实现:

 1 protected Class<?> loadClass(String name, boolean resolve)
 2         throws ClassNotFoundException
 3     {
 4         synchronized (getClassLoadingLock(name)) {
 5             // First, check if the class has already been loaded
 6             Class c = findLoadedClass(name);
 7             // 该类没有被加载
 8             if (c == null) {
 9                 long t0 = System.nanoTime();
10                 try {
11                     if (parent != null) {
12                     // 先交由父加载器尝试加载
13                         c = parent.loadClass(name, false);
14                     } else {
15                     // 父加载器为空,即为BootstrapClassLoader,那么查看启动类中是否有该类
16                         c = findBootstrapClassOrNull(name);
17                     }
18                 } catch (ClassNotFoundException e) {
19                     // ClassNotFoundException thrown if class not found
20                     // from the non-null parent class loader
21                 }
22                 //父类无法加载该类,则由自己尝试加载
23                 if (c == null) {
24                     // If still not found, then invoke findClass in order
25                     // to find the class.
26                     long t1 = System.nanoTime();
27                     c = findClass(name);
28
29                     // this is the defining class loader; record the stats
30                     sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
31                     sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
32                     sun.misc.PerfCounter.getFindClasses().increment();
33                 }
34             }
35             if (resolve) {
36                 resolveClass(c);
37             }
38             return c;
39         }
40     }

  可见首先会检查该类是否已经被加载,若没有被加载,则会委托父加载器进行装载,只有当父加载器无法加载时,才会调用自身的findClass()方法进行加载。这样避免了子加载器加载一些试图冒名顶替可信任类的不可靠类,也不会让子加载器去实现父加载器实现的加载工作。 
  比如某个用户自定义的类加载器试图加载一个叫做“java.lang.String”的类,那么,该类会最终委派给启动类加载器BootstrapClassLoader尝试加载,那么启动类加载器将加载Java API中的”java.lang.String”类而不会通过用户自定义的类加载器去获得和加载这个看上去不怀好意的冒名类。

  但是仅仅依赖双亲委派是远远不够的,假设这个用户自定义的类加载器试图加载一个叫做“java.lang.Bomb”的危险类,而父类加载器无法加载该类,那么加载工作将由用户定义的加载器负责实现。由于一个类的同一包内的类(和其子类)可以访问其protected成员,这个“java.lang.Bomb”则可能访问可信任类的一些敏感信息,所以就必须将这个类与可信任类的访问域隔离,Java虚拟机只把这样彼此访问的特殊权限授予由同一个类加载器加载的同一包内的类型,这样一个由同一个类加载器加载的、属于同一个包的多个类型集合称为运行时包。

2.命名空间

  类加载体系为不同类加载器加载的类提供不同的命名空间,同一命名空间内的类可以互相访问,不同命名空间的类不知道彼此的存在(除非显式提供访问机制)。同一类可以再不同的命名空间内,但无法在同一命名空间内重复出现。

命名空间是这样定义的:实际完成加载类的工作的加载器为定义类加载器,而加载的双亲委托路径上的所有加载器为初始类加载器,某个加载器的命名空间就是所有以该加载器为初始类加载器的类所组成。

可以预见,子加载器的命名空间包括其父/祖先加载器的命名空间和只有自己才可以加载的类所组成。根据加载体系结构的安全机制,同一命名空间内的类可以互相访问,所以父加载器所加载的类不一定可以访问子加载器所加载的类,但子加载器所加载的类必然可以访问父加载器加载的类。父加载器加载的类就好像小箱子,子加载器加载的类可能用到父加载器加载的类,就像一个大箱子,只能把小箱子放进大箱子,而不能反过来做(当然显式的访问机制除外)

以自己实现的类加载器为例:

  1 package com.ice.classloader;
  2
  3
  4 import java.io.ByteArrayOutputStream;
  5 import java.io.File;
  6 import java.io.FileInputStream;
  7 import java.io.FileNotFoundException;
  8 import java.io.IOException;
  9 import java.io.InputStream;
 10
 11 public class MyClassLoader extends ClassLoader {
 12
 13     private String name;    //加载器名称
 14     private String path = "E:\\WorkSpace\\ClassLoaderTest\\";  //加载路径
 15     private static final String HOME = "E:\\WorkSpace\\ClassLoaderTest\\";
 16     private final String classFileType = ".class";
 17
 18     public MyClassLoader(String name) {
 19         super();
 20         this.name = name;
 21     }
 22
 23     public MyClassLoader(ClassLoader parent, String name) {
 24         super(parent);
 25         this.name = name;
 26     }
 27
 28     @Override
 29     public String toString() {
 30         return this.name;
 31     }
 32
 33     public String getPath() {
 34         return path;
 35     }
 36
 37     public void setPath(String path) {
 38         this.path = path;
 39     }
 40
 41     @Override
 42     public Class<?> findClass(String name) throws ClassNotFoundException {
 43         byte[] data = this.loadClassData(name);
 44         if(data == null)
 45             throw new ClassNotFoundException();
 46         return this.defineClass(name, data, 0, data.length);
 47
 48     }
 49
 50     private byte[] loadClassData(String name) {
 51
 52         InputStream is = null;
 53         byte[] data = null;
 54         ByteArrayOutputStream baos = null;
 55 //        System.out.println("  classloader:" + this.name + " try to load");
 56         try {
 57             //类名转化为路径
 58             name = name.replace(".", "\\");
 59             is = new FileInputStream(new File(path + name + classFileType));
 60
 61             baos = new ByteArrayOutputStream();
 62             int ch = 0;
 63             while (-1 != (ch = is.read())) {
 64
 65                 baos.write(ch);
 66             }
 67
 68             data = baos.toByteArray();
 69         }
 70         catch (FileNotFoundException e) {
 71 //            e.printStackTrace();
 72             return null;
 73         }
 74         catch (IOException ioe) {
 75             ioe.printStackTrace();
 76       }
 77         finally {
 78             try {
 79                 is.close();
 80                 baos.close();
 81             }
 82             catch (Exception e2) {
 83             }
 84         }
 85         return data;
 86     }
 87
 88
 89     public static void main(String[] args) throws Exception {
 90         //假定的系统加载器
 91         MyClassLoader father = new MyClassLoader("father");
 92         father.setPath(HOME + "syslib\\");
 93
 94         MyClassLoader child = new MyClassLoader(father, "child");
 95         child.setPath(HOME + "ext\\");
 96
 97         MyClassLoader user = new MyClassLoader("user");
 98         user.setPath(HOME + "usr\\");
 99         System.out.println("-------------test parent--------------");
100         //测试父加载器关系
101         traverseParent(child);
102         System.out.println("-------------test load begin from child--------------");
103         //测试加载
104         test(child);
105         //测试命名空间
106         System.out.println("-------------test namespace--------------");
107         testNameSpace(user);
108
109     }
110
111     public static void traverseParent(ClassLoader loader) throws Exception{
112         if(loader == null) return;
113         System.out.println("travase classloader:" + loader.toString());
114         while(loader.getParent() != null){
115             System.out.println(loader.getParent());
116             loader = loader.getParent();
117         }
118     }
119
120     public static void test(ClassLoader loader) throws Exception {
121         Class<?> clazz = loader.loadClass("com.ice.classloader.LoadedClass");
122         Object object = clazz.newInstance();
123     }
124
125     public static void testNameSpace(ClassLoader loader) throws Exception {
126         Class<?> clazz = loader.loadClass("com.ice.classloader.LoadedClass");
127         Object object = clazz.newInstance();
128         try{
129             LoadedClass lc = (LoadedClass) object;
130         }catch(Exception e){
131             e.printStackTrace();
132         }
133     }
134 }

  被加载类LoadedClass的定义如下:

 1 //被加载类
 2 package com.ice.classloader;
 3
 4 public class LoadedClass {
 5
 6     public LoadedClass() {
 7         System.out.println("LoadedClass is loaded by: "
 8                 + this.getClass().getClassLoader());
 9
10     }
11
12 }

(1).双亲委派结果 
  child加载器会委托father进行加载,若father的加载目录下存在着对应的class文件,则会由父加载器father进行对应的加载工作(father也会交由AppClassLoader和ExtClassLoader尝试进行加载,但这两个加载器并不知道如何加载,故而最后会自己尝试进行加载)

  当father的加载目录下没有对应的class文件,则会交由child进行加载

(2).命名空间隔离 
  由于MyClassLoader是通过系统的(应用程序类加载器/类路径加载器加载的),而LoadedClass是由user加载器所加载的,AppClassLoader加载器是user加载器的父加载器,故由父加载器加载的类MyClassLoader无法看见子加载器user所加载的LoadedClass类,在MyClassLoader中尝试实例化LoadedClass类时就会出现如下错误:

  对应出错的正是尝试实例化LoadedClass类的那一行

128         try{
129             LoadedClass lc = (LoadedClass) object;
130         }catch(Exception e){

(3).运行时包 
  当请求加载一个com.ice.classloader.virus类时,AppClassLoader路径下没有该类的class文件,那么attaker加载器将会加载这个virus类,并暗示其为com.ice.classloader的一部分,该类想要获取com.ice.classloader包下被信任类的访问权限。但由于权限检查时,由于该Virus类由attacker加载而非AppClassLoader加载,故对MyClassLoader受保护成员的访问将会被阻止。

 1 package com.ice.classloader;
 2
 3 public class Virus {
 4
 5     public Virus() {
 6         System.out.println("Virus is loaded by: "
 7                 + this.getClass().getClassLoader());
 8         MyClassLoader cl = (MyClassLoader) this.getClass().getClassLoader();
 9         System.out.println("secret is:" + cl.secret);
10     }
11
12 }

  MyClassLoader 由AppClassLoader所加载,而Virus由用户自定义的加载器attacker所加载,虽然AppClassLoader是attacker的父加载器,即MyClassLoader对Virus可见,但由于两者不是由同一个加载器所加载,即不属于同一个运行时包,那么Virus对MyClassLoader的受保护成员访问受限

 1 public class MyClassLoader extends ClassLoader {
 2     protected int secret = -1;
 3 //...
 4     public static void main(String[] args) throws Exception {
 5             //其父加载器为Bootstrap ClassLoader
 6             MyClassLoader loader = new MyClassLoader(null, "loader");
 7             loader.setPath(HOME + "usr\\");
 8
 9             MyClassLoader attacker = new MyClassLoader("attacker");
10             attacker.setPath(HOME + "attacker\\");
11
12             System.out.println("MyClassLoader‘s classloader:" + MyClassLoader.class.getClassLoader());
13
14             System.out.println("-------------test parent--------------");
15             //测试父加载器关系
16             traverseParent(attacker);
17
18             System.out.println("-------------test in-package access--------------");
19             testVirus(attacker);
20
21         }
22
23         public static void traverseParent(ClassLoader loader) throws Exception{
24             if(loader == null) return;
25             System.out.println("travase classloader:" + loader.toString());
26             while(loader.getParent() != null){
27                 System.out.println(loader.getParent());
28                 loader = loader.getParent();
29             }
30         }
31
32
33         public static void testVirus(ClassLoader loader) throws Exception {
34             Class<?> clazz = loader.loadClass("com.ice.classloader.Virus");
35             Object object = clazz.newInstance();
36         }
37     }

结果如下:

  注意命名空间的隔离与运行时包隔离的区别,不同命名空间的类之间不可见,而同一命名空间内的类可能由不同的加载器进行加载,如启动类加载器加载的核心JavaAPI和用户自定义加载器加载的类,这些类及时声明定义为同一个包,但是由于不是由同一个加载器加载的,即不属于同一个运行时包,那么不同运行时包内的类之间就存在对包可见成员的访问限制。

3.策略与保护域 
  除了命名空间的访问隔离和双亲委派的受信类保护,类加载器体系还是用保护域来定义代码在运行时可以获得的权限。同样在分析保护域之前,先了解类Java虚拟机的安全访问控制及策略。

  Java的沙箱模型可以由用户自定义,这是通过用户定制沙箱的安全管理器(SecurityManager)来定义沙箱的安全边界,以为程序运行指定用户自定义的安全策略和访问控制。应用程序通过System.setSecurityManager()/“-Djava.security.manager”来指定/启动安全管理器,每当JavaAPI执行一些可能不安全的操作时,如对文件的读写和删除等,就会向安全管理器进行权限检查,若权限检查不通过,将会抛出一个安全异常,若权限检查通过,则允许该操作的执行。

  比如创建一个FileInputStream时,会调用SecurityManager的checkRead()进行读取权限的检查:

 1 public FileInputStream(File file) throws FileNotFoundException {
 2         String name = (file != null ? file.getPath() : null);
 3         SecurityManager security = System.getSecurityManager();
 4         if (security != null) {
 5             security.checkRead(name);
 6         }
 7         if (name == null) {
 8             throw new NullPointerException();
 9         }
10         fd = new FileDescriptor();
11         fd.incrementAndGetUseCount();
12         open(name);
13     }

  checkRead()即以读动作的FilePermission为参数调用checkPermission()

1 public void checkRead(String file) {
2         checkPermission(new FilePermission(file,
3             SecurityConstants.FILE_READ_ACTION));
4     }

  jdk1.2版本后,可以使用checkPermission(Permission perm)和checkPermission(Permission perm, Object context)来进行权限检查,其中perm为请求执行操作所需要的权限,如java.io.FilePermission对“/usr/indata.txt”请求“read”操作。checkPermission()实际上在对当前线程的方法栈进行优化后,获得一个访问控制环境AccessControlContext,并调用其checkPermission()方法

 1 public static void checkPermission(Permission perm)
 2                  throws AccessControlException
 3     {
 4         //System.err.println("checkPermission "+perm);
 5         //Thread.currentThread().dumpStack();
 6
 7         if (perm == null) {
 8             throw new NullPointerException("permission can‘t be null");
 9         }
10
11         AccessControlContext stack = getStackAccessControlContext();
12         // if context is null, we had privileged system code on the stack.
13         if (stack == null) {
14            //...debug相关
15             return;
16         }
17
18         AccessControlContext acc = stack.optimize();
19         acc.checkPermission(perm);
20     }

  checkPermission()会从方法的栈顶向栈底遍历(检查方法所在类的保护域权限,context是一个ProtectionDomain数组),当遇到一个没有权限的栈帧就会抛出一个AccessControlException。即对于一次需要进行权限检查的访问,对于该访问的方法的每一个调用层次都必须具有对应的访问权限。 
  对权限的判定是通过implies()来进行的,implies()在Permission类、PermissionCollection、ProtectionDomain类中声明。在Permission类(具体实现的子类)中,该方法将确定由该Permission所代表的对象,是否隐含了将要判断的Permission对象的权限中,如对”/test/*”目录的读写权限testAllPermission,隐含了对”/test/test.txt”文件的读写权限testFilePermission,即testAllPermission.implies(testFilePermission) 的值为true,反之为false。 在ProtectionDomain(其PermissionCollection)中,将进行权限集合内implies()的判定,实际上就是在PermissionCollection中遍历保护域所拥有的权限,调用implies()判定其是否具有对应的访问权限。

 1 public void checkPermission(Permission perm)
 2         throws AccessControlException
 3     {
 4        //...
 5         if (context == null)
 6             return;
 7
 8         for (int i=0; i< context.length; i++) {
 9             if (context[i] != null &&  !context[i].implies(perm)) {
10                 //...
11                 throw new AccessControlException("access denied "+perm, perm);
12             }
13         }
14
15         // ...
16
17         return;
18     }

那么,类的访问权限(保护域)是如何指定的? 
(1).类与访问权限是什么? 
每个class文件均和一个代码来源相关联,这个代码来源(java.security.CodeSource)通过URL类成员location指向代码库和对该class文件进行签名的零个或多个证书对象的数组(class文件在进行代码认证的过程中可能经过多个证书签名,也可能没有进行签名) 。 
访问控制策略Policy对权限的授予是以CodeSource为基础进行的,每个CodeSource拥有若干个Permission,这些Permission对象会被具体地以其子类,如FilePermission、SocketPermission等描述,并且和CodeSource相关联的Permission对象将被封装在java.security.PermissionCollection(抽象类)的一个子类实例中,以描述该CodeSource所获取的权限。 
(2).从类的加载到保护域探寻类访问权限的指定:
加载器会调用defineClass解析和加载类的Class实例:

 1 protected final Class<?> defineClass(String name, byte[] b, int off, int len,
 2                                          ProtectionDomain protectionDomain)
 3         throws ClassFormatError
 4     {
 5         protectionDomain = preDefineClass(name, protectionDomain);
 6
 7         Class c = null;
 8         String source = defineClassSourceLocation(protectionDomain);
 9
10         try {
11             c = defineClass1(name, b, off, len, protectionDomain, source);
12         } catch (ClassFormatError cfe) {
13             c = defineTransformedClass(name, b, off, len, protectionDomain, cfe,
14                                        source);
15         }
16
17         postDefineClass(c, protectionDomain);
18         return c;
19     }

在defineClass()中,会调用preDefineClass()获取ProtectionDomain:

 1 private ProtectionDomain preDefineClass(String name,
 2                                             ProtectionDomain pd)
 3     {
 4         if (!checkName(name))
 5             throw new NoClassDefFoundError("IllegalName: " + name);
 6
 7         if ((name != null) && name.startsWith("java.")) {
 8             throw new SecurityException
 9                 ("Prohibited package name: " +
10                  name.substring(0, name.lastIndexOf(‘.‘)));
11         }
12         if (pd == null) {
13             pd = defaultDomain;
14         }
15
16         if (name != null) checkCerts(name, pd.getCodeSource());
17
18         return pd;
19     }

当没有指定保护域时,就会为其指定一个空的保护域,若指定了保护域则使用加载器所指定的保护域。

类加载器的实现可以通过将代码来源(CodeSource),即代码库和该class文件的所有签名者信息,传递给当前的Policy对象的getPermissions()方法,来查询该代码来源所拥有的权限集合PermissionCollection(在策略初始化时生成),并以此构造一个保护域传递给defineClass(),以此指定类的保护域。

(3).Java应用程序访问控制策略是由抽象类java.security.Policy的子类实例所描述的,通过设置policy.provider属性值来指定Policy的实现类,该属性值定义在/jre/lib/security/java.security文件中

#
# Class to instantiate as the system Policy. This is the name of the class
# that will be used as the Policy object.
#
policy.provider=sun.security.provider.PolicyFile

可见默认是使用PolicyFile类来实现访问控制策略,该类将使用从策略文件中读取并解析访问控制策略的方式形成策略。 
也可以通过实现自己的Policy并调用Policy的setPolicy()方法来替换当前Policy对象。

对Java应用程序的访问控制策略是由抽象类java.security.Policy的子类实例实现的,其实现方式可以采用很多种方法,如从一个结构化ASCII文件中读取,从一个Policy的二进制class文件中读取,从一个数据库中读取,PolicyFile就是使用了从ASCII策略文件中读取的方法,策略文件定义在/jre/lib/security/java.security中:

1 # The default is to have a single system-wide policy file,
2 # and a policy file in the user‘s home directory.
3 policy.url.1=file:${java.home}/lib/security/java.policy
4 policy.url.2=file:${user.home}/.java.policy

可以在java.security文件中修改或添加policy.url.x来指定用户自己想要的策略,也可以在运行时使用”-Djava.security.policy”命令行参数进行设置,如: 
-Djava.security.manager -Djava.security.policy = mypolicy.txt 
其中如果没有指定java.security.manager,那么应用程序就不会安装任何的安全管理器,而代码也就没有任何权限限制。mypolicy.txt就是用户自顶一个策略文件,这里使用的是相对路径,将使用程序的启动目录

以/jre/lib/security/java.policy为例说明策略文件,在该文件中使用上下文无关文法描述安全策略 
如:

grant codeBase "file:${{java.ext.dirs}}/*" {
    permission java.security.AllPermission;
};

policy文件的基本语法如下:

keystore "keystore_url",
"keystore_type";

grant [SignedBy "signer_names"] [, CodeBase "URL"] [,principal principal_class_name "principal_name",]{
Permission permission_class_name
[ "target_name" ]
[, "action"] [, SignedBy "signer_names"];
Permission ...
};
  • keystore: 
    keystore_url指定了签名者的公钥的证书文件所在位置,可以使相对URL,如keystore “mykey”,这个相对路径指向了程序使用该策略文件的启动目录,比如,该策略文件由policy.url.x指定在”e://security/policy/mypolicy.txt”,那么公钥证书就在”e://security/policy/mykey”文件中。当然也可以使用绝对路径指定公钥路径。 
    keystore_type指定了密钥仓库信息的存储和数据格式,也定义了保护密钥仓库中私钥和密钥仓库完整性的算法,默认将使”JKS”类型
  • grant子句: 
    授予 指定类型(代码) 指定权限 
    其中对代码类型的描述有两种: 
    signedBy表示签名者别名,可以是由”,”分隔的若干个签名者 
    codeBase表示一个特定的加载位置,从该目录下加载的代码都将被赋予特定的权限
  • permission: 
    permission由权限类型、操作目标、操作动作三部分组成,如 
    permission java.io.FilePermission “note.txt” “read”即为对程序启动目录下(相对路径)的note.txt的读取权限

最后以深入jvm(第二版)一书中的例子来介绍策略文件的使用以及保护域的作用:

Doer接口:

1 // /com/ice/security/doer/Doer.java
2 package com.ice.security.doer;
3
4 public interface Doer {
5     void doYourThing();
6 }

Doer的实现类Friend,由Friend所签名,将作为受信认类访问“friend.txt”和“stranger.txt”

 1 // /com/ice/security/friend/Friend.java
 2 package com.ice.security.friend;
 3
 4 import java.security.AccessController;
 5 import java.security.PrivilegedAction;
 6
 7 import com.ice.security.doer.Doer;
 8
 9 public class Friend implements Doer{
10     private Doer next;
11     private boolean direct;
12
13     public Friend(Doer next, boolean direct){
14         this.next = next;
15         this.direct = direct;
16     }
17
18     @Override
19     public void doYourThing() {
20         if(direct){
21             next.doYourThing();
22         }else{
23             AccessController.doPrivileged(
24                     new PrivilegedAction() {
25                         public Object run(){
26                             next.doYourThing();
27                             return null;
28                         }
29                     }
30             );
31         }
32     }
33
34 }

Doer的实现类Stranger,由Stranger所签名,作为不受信认类,仅能访问“stranger.txt”

//com/ice/security/stranger/Stranger.java
package com.ice.security.stranger;

import java.security.AccessController;
import java.security.PrivilegedAction;

import com.ice.security.doer.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() {
        if(direct){
            next.doYourThing();
        }else{
            AccessController.doPrivileged(
                    new PrivilegedAction() {
                        public Object run(){
                            next.doYourThing();
                            return null;
                        }
                    }
            );
        }
    }

}

txt文件的显示输出类TextFileDisplayer:

 1 //TextFileDisplayer.java
 2 import java.io.CharArrayWriter;
 3 import java.io.FileReader;
 4 import java.io.IOException;
 5
 6 import com.ice.security.doer.Doer;
 7
 8
 9 public class TextFileDisplayer implements Doer{
10     private String fileName;
11     public TextFileDisplayer(String fileName){
12         this.fileName = fileName;
13     }
14
15     @Override
16     public void doYourThing() {
17             try{
18                 FileReader fr = new FileReader(fileName);
19                 try {
20                     CharArrayWriter caw = new CharArrayWriter();
21                     int c;
22                     while((c = fr.read()) != -1){
23                         caw.write(c);
24                     }
25                     System.out.println(caw.toString());
26                 } catch (IOException e) {
27
28                 }finally{
29                     try{
30                         fr.close();
31                     }catch (IOException e){
32
33                     }
34                 }
35             }catch (IOException e) {
36
37             }
38     }
39
40 }

1.将Friend和Stranger分别导出为jar文件,放在指定目录(这里放在”E:\java\security”目录下)以待不同的机构进行签名,Friend所在包假定为比较有信用的机构”friend”进行签名,而Stranger所在包假定为一个不受信任的机构”stranger”进行签名。 
(1).调用命令 jar cvf xxx.jar <_ClassPath> 进行打包 
(注意打包后,若没有把jar包放在单独的目录下,需要删除原java文件编译产生的class文件,以免程序运行直接加载目录下class文件而非包内的class文件) 
这里分别调用 
jar cvf friend.jar com/ice/security/friend/*.class 将friend包内的class文件打包

jar cvf stranger.jar com/ice/security/stranger/*.class 将friend包内的class文件打包

(2).使用keytool可以用来生成新的密钥对,并与一个别名关联,用密码加以保护存放在keystore文件中 
使用keytool -genkey -alias friend -keypass 123456 -validity 10000 -keystore mykey 命令:

该密钥的别名是friend,别名密码是123456(至少6位),有效期是10000天,存放在一个mykey的keystore文件中,keystore密码为myfriendkey

类似地,生成一个别名stranger的密钥对

为了方便起见,两个不同的签名者stranger和friend的密钥均存放在mykey中,mykey的访问密码是myfriendkey,密钥的访问密码都是123456

可以看到在目录下生成了一个mykey文件 
(3).使用jarsigner -keystore -storepass -keypass 命令进行签名 
这里: 
jarsigner -keystore mykey -storepass myfriendkey -keypass 123456 friend.jar friend 
jarsigner -keystore mykey -storepass myfriendkey -keypass 123456 stranger.jar stranger 
使用friend密钥对friend.jar进行签名,使用stranger密钥对stranger.jar进行签名

(4).最后可以使用 
keytool -export -alias -storepass -file -keystore 
这里分别用: 
keytool -export -alias friend -storepass myfriendkey -file friend.cer -keystore mykey 
keytool -export -alias stranger -storepass myfriendkey -file stranger.cer -keystore mykey 
导出friend和stranger的发行证书

2.编写自己的策略文件,放在当前目录下

//mypolicy.txt
keystore "mykey";

grant signedBy "friend"{
    permission java.io.FilePermission "friend.txt","read";
    permission java.io.FilePermission "stranger.txt","read";
};

grant signedBy "stranger"{
    permission java.io.FilePermission "stranger.txt","read";
};

grant codeBase "file:${com.ice.home}/com*"{
    permission java.io.FilePermission "friend.txt","read";
    permission java.io.FilePermission "stranger.txt","read";
};

这里friend签名的类和${com.ice.home}.com(后面设置为”e:\java\security\com”,存放着Doer接口的class文件)可以读取”friend.txt”和”stranger.txt”,而stranger签名的类只能读取”stranger.txt” 
(这里为了方便,直接使用mykey而非发布的证书) 
(1).添加Doer接口类的class文件(对应路径)和friend.txt和stranger.txt两个测试文件 
(2).通过权限检查的例子:

1 public class ProtectionDomainTest {
2     public static void main(String[] args){
3     TextFileDisplayer tfd = new TextFileDisplayer("stranger.txt");
4     Friend friend = new Friend(tfd, true);
5     Stranger stranger = new Stranger(friend, true);
6     stranger.doYourThing();
7     }
8 }

调用java -Djava.security.manager -Djava.security.policy=mypolicy.txt -Dcom.ice.home=e:\java\security -cp .;friend.jar;stranger.jar ProtectionDomainTest测试运行,其中指定了com.ice.home的路径,通过-cp设置了类路径

(3).不能通过权限检查的例子:

1 public class ProtectionDomainTest {
2     public static void main(String[] args){
3     TextFileDisplayer tfd = new TextFileDisplayer("friend.txt");
4     Friend friend = new Friend(tfd, true);
5     Stranger stranger = new Stranger(friend, true);
6     stranger.doYourThing();
7     }
8 }

与(2)类似,但stranger会尝试让friend读取”friend.txt”,这会被阻止

时间: 2024-11-05 05:23:31

实例分析JVM安全体系:双亲委派、命名空间、保护域、策略的相关文章

JVM中classloader双亲委派

public class Test { public static void main(String[] args) { // TODO Auto-generated method stub ClassLoader cl = Test.class.getClassLoader(); while(cl!=null) { System.out.println(cl.getClass().getName()); cl = cl.getParent(); } System.out.println(cl)

深入JVM系列(三)之类加载、类加载器、双亲委派机制与常见问题

一.概述 定义:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的java类型.类加载和连接的过程都是在运行期间完成的. 二. 类的加载方式 1):本地编译好的class中直接加载 2):网络加载:java.net.URLClassLoader可以加载url指定的类 3):从jar.zip等等压缩文件加载类,自动解析jar文件找到class文件去加载util类 4):从java源代码文件动态编译成为class文件 三.类加载的时机

为什么JVM的类加载要采用双亲委派的加载机制?

为什么JVM要采用双亲委派机制加载类呢? 任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间. 也就是说,判断2个类是否“相等”,只有在这2个类是由同一个类加载器加载的前提下才有意义,否则即使这2个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,这2个类必定不相等. 基于双亲委派模型设计,那么Java中基础的类,Object类似Object类重复多次的问题就不会存在了,因为经过层层传递,加

JVM加载类的过程,双亲委派机制中的方法

JVM加载类的过程: 1)JVM中类的整个生命周期: 加载=>验证=>准备=>解析=>初始化=>使用=>卸载  1.1.加载 类的加载阶段,主要是获取定义此类的二进制字节流,并将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,最后在Java堆中生成一个代表这个类的java.lang.Class对象作为方法区这些数据的访问入口.相对于类加载过程的其他阶段,加载阶段是开发期可控性最强的阶段.我们可以通过定制不通的类加载器,也就是ClassLoader来控制二进制

【深入理解JVM】类加载器与双亲委派模型

原文链接:http://blog.csdn.net/u011080472/article/details/51332866,http://www.cnblogs.com/lanxuezaipiao/p/4138511.html 加载类的开放性 类加载器(ClassLoader)是Java语言的一项创新,也是Java流行的一个重要原因.在类加载的第一阶段"加载"过程中,需要通过一个类的全限定名来获取定义此类的二进制字节流,完成这个动作的代码块就是类加载器.这一动作是放在Java虚拟机外部

[jvm解析系列][十]类加载器和双亲委派模型,你真的了解ClassLoader吗?

上一章我们讲到,一个类加载到内存里我们可以操作的部分只有两个,一个是加载部分一个是static{},我相信static{}不用多讲了. 接下来我们就来解析一下ClassLoader即类加载器,他就是用来加载字节码到方法区的类. 当年出现ClassLoader这个东西动态加载类的字节码主要还是为了满足JavaApplet的需求.虽然后来JavaApplet挂掉了,但是ClassLoader这个形式还是保留了下来,而且活的很好. 类的相等和instanceOf: 来我们来写一个例子 public c

【深入理解JVM】:类加载器与双亲委派模型

类加载器 加载类的开放性 类加载器(ClassLoader)是Java语言的一项创新,也是Java流行的一个重要原因.在类加载的第一阶段"加载"过程中,需要通过一个类的全限定名来获取定义此类的二进制字节流,完成这个动作的代码块就是类加载器.这一动作是放在Java虚拟机外部去实现的,以便让应用程序自己决定如何获取所需的类. 虚拟机规范并没有指明二进制字节流要从一个Class文件获取,或者说根本没有指明从哪里获取.怎样获取.这种开放使得Java在很多领域得到充分运用,例如: 从ZIP包中读

JVM类加载机制详解(二)类加载器与双亲委派模型

在上一篇JVM类加载机制详解(一)JVM类加载过程中说到,类加载机制的第一个阶段加载做的工作有: 1.通过一个类的全限定名(包名与类名)来获取定义此类的二进制字节流(Class文件).而获取的方式,可以通过jar包.war包.网络中获取.JSP文件生成等方式. 2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构.这里只是转化了数据结构,并未合并数据.(方法区就是用来存放已被加载的类信息,常量,静态变量,编译后的代码的运行时内存区域) 3.在内存中生成一个代表这个类的java.lan

[五]类加载机制双亲委派机制 底层代码实现原理 源码分析 java类加载双亲委派机制是如何实现的

Launcher启动类 本文是双亲委派机制的源码分析部分,类加载机制中的双亲委派模型对于jvm的稳定运行是非常重要的 不过源码其实比较简单,接下来简单介绍一下 我们先从启动类说起 有一个Launcher类   sun.misc.Launcher; 仔细看下这简短的几行注释,可以得到有用的信息 ps:直接IDE里面查看反编译的,看不到注释的,可以下载openJDK查看源码,我的这个版本是openjdk-8-src-b132-03_mar_2014 sun.misc.Launcher这个类是系统用于