在了解双亲委派模型之前,先了解一下类加载器的概念:
类加载器的作用就是将真实的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()方法实现:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class c = findLoadedClass(name); // 该类没有被加载 if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { // 先交由父加载器尝试加载 c = parent.loadClass(name, false); } else { // 父加载器为空,即为BootstrapClassLoader,那么查看启动类中是否有该类 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } //父类无法加载该类,则由自己尝试加载 if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
可见首先会检查该类是否已经被加载,若没有被加载,则会委托父加载器进行装载,只有当父加载器无法加载时,才会调用自身的findClass()方法进行加载。这样避免了子加载器加载一些试图冒名顶替可信任类的不可靠类,也不会让子加载器去实现父加载器实现的加载工作。比如某个用户自定义的类加载器试图加载一个叫做“java.lang.String”的类,那么,该类会最终委派给启动类加载器 BootstrapClassLoader尝试加载,那么启动类加载器将加载Java API中的”java.lang.String”类而不会通过用户自定义的类加载器去获得和加载这个看上去不怀好意的冒名类。
但是仅仅依赖双亲委派是远远不够的,假设这个用户自定义的类加载器试图加载一个叫做“java.lang.Bomb”的危险类,而父类加载器无 法加载该类,那么加载工作将由用户定义的加载器负责实现。由于一个类的同一包内的类(和其子类)可以访问其protected成员,这个 “java.lang.Bomb”则可能访问可信任类的一些敏感信息,所以就必须将这个类与可信任类的访问域隔离,Java虚拟机只把这样彼此访问的特殊 权限授予由同一个类加载器加载的同一包内的类型,这样一个由同一个类加载器加载的、属于同一个包的多个类型集合称为运行时包。
2.命名空间
类加载体系为不同类加载器加载的类提供不同的命名空间,同一命名空间内的类可以互相访问,不同命名空间的类不知道彼此的存在(除非显式提供访问机制)。同一类可以再不同的命名空间内,但无法在同一命名空间内重复出现。
命名空间是这样定义的:实际完成加载类的工作的加载器为定义类加载器,而加载的双亲委托路径上的所有加载器为初始类加载器,某个加载器的命名空间就是所有以该加载器为初始类加载器的类所组成。
可以预见,子加载器的命名空间包括其父/祖先加载器的命名空间和只有自己才可以加载的类所组成。根据加载体系结构的安全机制,同一命名空间内的类可 以互相访问,所以父加载器所加载的类不一定可以访问子加载器所加载的类,但子加载器所加载的类必然可以访问父加载器加载的类。父加载器加载的类就好像小箱 子,子加载器加载的类可能用到父加载器加载的类,就像一个大箱子,只能把小箱子放进大箱子,而不能反过来做(当然显式的访问机制除外)
以自己实现的类加载器为例:
package com.ice.classloader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; public class MyClassLoader extends ClassLoader { private String name; //加载器名称 private String path = "E://WorkSpace//ClassLoaderTest//"; //加载路径 private static final String HOME = "E://WorkSpace//ClassLoaderTest//"; private final String classFileType = ".class"; public MyClassLoader(String name) { super(); this.name = name; } public MyClassLoader(ClassLoader parent, String name) { super(parent); this.name = name; } @Override public String toString() { return this.name; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } @Override public Class<?> findClass(String name) throws ClassNotFoundException { byte[] data = this.loadClassData(name); if(data == null) throw new ClassNotFoundException(); return this.defineClass(name, data, 0, data.length); } private byte[] loadClassData(String name) { InputStream is = null; byte[] data = null; ByteArrayOutputStream baos = null; // System.out.println(" classloader:" + this.name + " try to load"); try { //类名转化为路径 name = name.replace(".", "//"); is = new FileInputStream(new File(path + name + classFileType)); baos = new ByteArrayOutputStream(); int ch = 0; while (-1 != (ch = is.read())) { baos.write(ch); } data = baos.toByteArray(); } catch (FileNotFoundException e) { // e.printStackTrace(); return null; } catch (IOException ioe) { ioe.printStackTrace(); } finally { try { is.close(); baos.close(); } catch (Exception e2) { } } return data; } public static void main(String[] args) throws Exception { //假定的系统加载器 MyClassLoader father = new MyClassLoader("father"); father.setPath(HOME + "syslib//"); MyClassLoader child = new MyClassLoader(father, "child"); child.setPath(HOME + "ext//"); MyClassLoader user = new MyClassLoader("user"); user.setPath(HOME + "usr//"); System.out.println("-------------test parent--------------"); //测试父加载器关系 traverseParent(child); System.out.println("-------------test load begin from child--------------"); //测试加载 test(child); //测试命名空间 System.out.println("-------------test namespace--------------"); testNameSpace(user); } public static void traverseParent(ClassLoader loader) throws Exception{ if(loader == null) return; System.out.println("travase classloader:" + loader.toString()); while(loader.getParent() != null){ System.out.println(loader.getParent()); loader = loader.getParent(); } } public static void test(ClassLoader loader) throws Exception { Class<?> clazz = loader.loadClass("com.ice.classloader.LoadedClass"); Object object = clazz.newInstance(); } public static void testNameSpace(ClassLoader loader) throws Exception { Class<?> clazz = loader.loadClass("com.ice.classloader.LoadedClass"); Object object = clazz.newInstance(); try{ LoadedClass lc = (LoadedClass) object; }catch(Exception e){ e.printStackTrace(); } } }
被加载类LoadedClass的定义如下:
//被加载类 package com.ice.classloader; public class LoadedClass { public LoadedClass() { System.out.println("LoadedClass is loaded by: " + this.getClass().getClassLoader()); } }
(1).双亲委派结果child加载器会委托father进行加载,若father的加载目录下存在着对应的class文件,则会由 父加载器father进行对应的加载工作(father也会交由AppClassLoader和ExtClassLoader尝试进行加载,但这两个加载 器并不知道如何加载,故而最后会自己尝试进行加载)
当father的加载目录下没有对应的class文件,则会交由child进行加载
(2).命名空间隔离由于MyClassLoader是通过系统的(应用程序类加载器/类路径加载器加载的),而 LoadedClass是由user加载器所加载的,AppClassLoader加载器是user加载器的父加载器,故由父加载器加载的类 MyClassLoader无法看见子加载器user所加载的LoadedClass类,在MyClassLoader中尝试实例化 LoadedClass类时就会出现如下错误:
对应出错的正是尝试实例化LoadedClass类的那一行
try{ LoadedClass lc = (LoadedClass) object; }catch(Exception e){
(3).运行时包当请求加载一个com.ice.classloader.virus类时,AppClassLoader路径下没有 该类的class文件,那么attaker加载器将会加载这个virus类,并暗示其为com.ice.classloader的一部分,该类想要获取 com.ice.classloader包下被信任类的访问权限。但由于权限检查时,由于该Virus类由attacker加载而非 AppClassLoader加载,故对MyClassLoader受保护成员的访问将会被阻止。
package com.ice.classloader; public class Virus { public Virus() { System.out.println("Virus is loaded by: " + this.getClass().getClassLoader()); MyClassLoader cl = (MyClassLoader) this.getClass().getClassLoader(); System.out.println("secret is:" + cl.secret); } }
MyClassLoader 由AppClassLoader所加载,而Virus由用户自定义的加载器attacker所加载,虽然AppClassLoader是attacker 的父加载器,即MyClassLoader对Virus可见,但由于两者不是由同一个加载器所加载,即不属于同一个运行时包,那么Virus对 MyClassLoader的受保护成员访问受限
public class MyClassLoader extends ClassLoader { protected int secret = -1; //... public static void main(String[] args) throws Exception { //其父加载器为Bootstrap ClassLoader MyClassLoader loader = new MyClassLoader(null, "loader"); loader.setPath(HOME + "usr//"); MyClassLoader attacker = new MyClassLoader("attacker"); attacker.setPath(HOME + "attacker//"); System.out.println("MyClassLoader's classloader:" + MyClassLoader.class.getClassLoader()); System.out.println("-------------test parent--------------"); //测试父加载器关系 traverseParent(attacker); System.out.println("-------------test in-package access--------------"); testVirus(attacker); } public static void traverseParent(ClassLoader loader) throws Exception{ if(loader == null) return; System.out.println("travase classloader:" + loader.toString()); while(loader.getParent() != null){ System.out.println(loader.getParent()); loader = loader.getParent(); } } public static void testVirus(ClassLoader loader) throws Exception { Class<?> clazz = loader.loadClass("com.ice.classloader.Virus"); Object object = clazz.newInstance(); } }
结果如下:
注意命名空间的隔离与运行时包隔离的区别,不同命名空间的类之间不可见,而同一命名空间内的类可能由不同的加载器进行加载,如启动类加载器加载 的核心JavaAPI和用户自定义加载器加载的类,这些类及时声明定义为同一个包,但是由于不是由同一个加载器加载的,即不属于同一个运行时包,那么不同 运行时包内的类之间就存在对包可见成员的访问限制。
3.策略与保护域
除了命名空间的访问隔离和双亲委派的受信类保护,类加载器体系还是用保护域来定义代码在运行时可以获得的权限。同样在分析保护域之前,先了解类Java虚拟机的安全访问控制及策略。
Java的沙箱模型可以由用户自定义,这是通过用户定制沙箱的安全管理器(SecurityManager)来定义沙箱的安全边界,以为程序运 行指定用户自定义的安全策略和访问控制。应用程序通过 System.setSecurityManager()/“-Djava.security.manager”来指定/启动安全管理器,每当 JavaAPI执行一些可能不安全的操作时,如对文件的读写和删除等,就会向安全管理器进行权限检查,若权限检查不通过,将会抛出一个安全异常,若权限检 查通过,则允许该操作的执行。
比如创建一个FileInputStream时,会调用SecurityManager的checkRead()进行读取权限的检查:
public FileInputStream(File file) throws FileNotFoundException { String name = (file != null ? file.getPath() : null); SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkRead(name); } if (name == null) { throw new NullPointerException(); } fd = new FileDescriptor(); fd.incrementAndGetUseCount(); open(name); }
checkRead()即以读动作的FilePermission为参数调用checkPermission()
public void checkRead(String file) { checkPermission(new FilePermission(file, SecurityConstants.FILE_READ_ACTION)); }
jdk1.2版本后,可以使用checkPermission(Permission perm)和checkPermission(Permission perm, Object context)来进行权限检查,其中perm为请求执行操作所需要的权限,如java.io.FilePermission对“/usr /indata.txt”请求“read”操作。checkPermission()实际上在对当前线程的方法栈进行优化后,获得一个访问控制环境 AccessControlContext,并调用其checkPermission()方法
public static void checkPermission(Permission perm) throws AccessControlException { //System.err.println("checkPermission "+perm); //Thread.currentThread().dumpStack(); if (perm == null) { throw new NullPointerException("permission can't be null"); } AccessControlContext stack = getStackAccessControlContext(); // if context is null, we had privileged system code on the stack. if (stack == null) { //...debug相关 return; } AccessControlContext acc = stack.optimize(); acc.checkPermission(perm); }
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()判定其是否具有对应的访问权限。
public void checkPermission(Permission perm) throws AccessControlException { //... if (context == null) return; for (int i=0; i< context.length; i++) { if (context[i] != null && !context[i].implies(perm)) { //... throw new AccessControlException("access denied "+perm, perm); } } // ... return; }
那么,类的访问权限(保护域)是如何指定的?
(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实例:
protected final Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain) throws ClassFormatError { protectionDomain = preDefineClass(name, protectionDomain); Class c = null; String source = defineClassSourceLocation(protectionDomain); try { c = defineClass1(name, b, off, len, protectionDomain, source); } catch (ClassFormatError cfe) { c = defineTransformedClass(name, b, off, len, protectionDomain, cfe, source); } postDefineClass(c, protectionDomain); return c; }
在defineClass()中,会调用preDefineClass()获取ProtectionDomain:
private ProtectionDomain preDefineClass(String name, ProtectionDomain pd) { if (!checkName(name)) throw new NoClassDefFoundError("IllegalName: " + name); if ((name != null) && name.startsWith("java.")) { throw new SecurityException ("Prohibited package name: " + name.substring(0, name.lastIndexOf('.'))); } if (pd == null) { pd = defaultDomain; } if (name != null) checkCerts(name, pd.getCodeSource()); return pd; }
当没有指定保护域时,就会为其指定一个空的保护域,若指定了保护域则使用加载器所指定的保护域。
类加载器的实现可以通过 将代码来源(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 ... };
最后以深入jvm(第二版)一书中的例子来介绍策略文件的使用以及保护域的作用:
Doer接口:
// /com/ice/security/doer/Doer.java package com.ice.security.doer; public interface Doer { void doYourThing(); }
Doer的实现类Friend,由Friend所签名,将作为受信认类访问“friend.txt”和“stranger.txt”
// /com/ice/security/friend/Friend.java package com.ice.security.friend; import java.security.AccessController; import java.security.PrivilegedAction; import com.ice.security.doer.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() { if(direct){ next.doYourThing(); }else{ AccessController.doPrivileged( new PrivilegedAction() { public Object run(){ next.doYourThing(); return null; } } ); } } }
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:
//TextFileDisplayer.java import java.io.CharArrayWriter; import java.io.FileReader; import java.io.IOException; import com.ice.security.doer.Doer; public class TextFileDisplayer implements Doer{ private 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) { }finally{ try{ fr.close(); }catch (IOException e){ } } }catch (IOException e) { } } }
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).通过权限检查的例子:
public class ProtectionDomainTest { public static void main(String[] args){ TextFileDisplayer tfd = new TextFileDisplayer("stranger.txt"); Friend friend = new Friend(tfd, true); Stranger stranger = new Stranger(friend, true); stranger.doYourThing(); } }
调用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).不能通过权限检查的例子:
public class ProtectionDomainTest { public static void main(String[] args){ TextFileDisplayer tfd = new TextFileDisplayer("friend.txt"); Friend friend = new Friend(tfd, true); Stranger stranger = new Stranger(friend, true); stranger.doYourThing(); } }
与(2)类似,但stranger会尝试让friend读取”friend.txt”,这会被阻止