java最初的设计目标是网络应用, 所以安全尤为重要. java安全模型主要集中在防止终端用户受到不被信任的程序的破坏. 为了达到这个目标, java提供了一个称之为"沙箱"的模型. 一个java程序必须在沙箱边界内运行. 沙箱组件包括: 类加载器体系结构, class文件验证, java内置的安全特性, 安全管理器以及API.
类装载器体系结构
在Java沙箱中,类装载器体系结构式第一道防线,起到这些作用: 1 守护被信任的类库, 2 防止恶意代码干涉善意代码. 守护被信任的类库是通过双亲委派模式实现的. 每个类装载器都有一个parent, 除了根加载器(bootstrap classLoader),用户自定义的类加载器的parent默认是类路径类加载器(AppClassLoader), 类路径类加载器的parent是拓展类加载器(ExtClassLoader), 拓展类加载器的parent是启动类加载器(BootstrapClassLoader). 你可以自己写一个程序(class.getClassLoader().getParent())看看这些类加载器的类型和关系.
启动(Bootstrap)类装载器:启动类装载器是用本地代码实现的类装载器,它是虚拟机的一部分, 代码中你会发现, ExtClassLoader的parent是null, 这是因为它不是java类的实例. 它负责将< Java_Runtime_Home >/lib 下面的类库加载到内存中。由于启动类装载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类装载器的引用,所以不允许直接通过引用进行操作。
扩展(Extension)类装载器:标准扩展类装载器是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader) 实现的。它负责将 < Java_Runtime_Home >/lib/ext 或者由系统变量 java.ext.dir 指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类装载器.
类路径(ClassPath)类装载器:类路径类装载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器.
用户自定义类加载器: 即用户自己用java写的, 继承自ClassLoader的类加载器, 它默认的parent是AppClassLoader, 用户可以以自己的方式,甚至可以用代码动态生成类.
在有双亲委派模式的情况下,启动类装载器可以抢在标准扩展类装载器之前去装载系统核心类库,而标准扩展类装载器可以抢在类路径类装载器之前去装载拓展类库,类路径类装载器又可以抢在用户自定义类装载器之前去装载CLASSPATH下的类,用这种方法,类装载器的体系结构就可以防止不可靠的代码用它们自己的版本来替代可信任的类. 如有人自己写了一个java.lang.String, 而且放在classpath中, 当程序用到String的时候, 则AppClassLoader首先会委托ExtClassLoader来加载, 而ExtClassLoader会委托BootstrapClassLoader加载, BootstrapClassLoader恰好会在<java_rumtime_home>/lib下找到叫做java.lang.String的类并加载进来, 所以那个自己写的String根本没有用武之地. 如果想在被信任的包里面加入一个类来达到被加载的目的呢? 这招也行不通, 比如自己写一个java.lang.Virus, 则会抛出SecurityException: Prohibited package name: java.lang. 事实上每个类加载器都会给一个被限制的包的列表.
防止恶意代码干涉善意代码是通过命名空间来实现的. JVM为每个类加载器维护一个命名空间, 不同的类加载器加载的类会被放在不同的命名空间, 不同命名空间里面的类感觉不到其他命名空间的类的存在. 如你写了一个类Vocano, 通过你自己的类加载器加载, 而有黑客想写一个有恶意代码的Vocano也被加载进来, 但是因为同一个命名空间里叫做VOcano的类只能有一个, 所以有恶意代码的Vocano不能由你指定的类加载器加载, 它必须由其他类加载器加载, 而其他类加载器加载的恶意的Vocano对你的Vocano不会有任何影响.
Class文件检验
Class文件检验是在字节码被执行之前进行的,以保证class文件的正确性,总共需要进行4趟扫描来完成。第一趟验证结构的完整性,第二趟验证语义,第三趟验证字节码, 第四趟验证符号引用的正确性。
第一趟 验证结构的完整性:class文件检查器会验证class文件是否符合class文件的基本结构,比如是否以0XCAFEBABE开头,若不是,则不合格。 class文件检查器还要验证class文件是否有删减, 是否正常结束, 尾部有没有附带不应该出现的字节。 虽然class文件长度不固定,但是class文件的每个部分都声明了长度和类型。基于此, class文件检查器可确定该文件是否有删减,是否正常结束。
第二趟 验证语义:在第二趟扫描中,class文件检查器进行的检查不需要查看字节码,也不需要查看和装载任何其他类型:在这趟扫描中,检验器查看每个组成部分,确认它们是否是其所属类型的实例,它们的结构是否正确。检验器对每个组成部分进行检查的目的之一是,为了确认每个方法描述都是符合特定语法的、格式正确的字符串。
第三趟 字节码验证: (这段原书照抄,我没学过汇编,理解的不是很好,无法提炼)字节码流代表了Java的方法,它是由被称为操作码的单字节指令组成的序列,每一个操作码后都跟着一个或多个操作数。操作数用于在Java虚拟机执行操作码指令时提供的额外的数据。执行字节码时,依次执行每个操作码,这就在Java虚拟机内构成了执行的线程。每一个线程被授予自己的Java栈,这个栈是由不同的栈帧构成的,每一个方法调用将获得一个自己的栈帧,栈帧其实就是一个内存片段,其中存储着局部变量和计算的中间结果。在栈帧中用于存储方法的中间结果的部分被称为该方法的操作数栈。操作码和它的(可选的)操作数可能指存储在操作数栈中的数据,或存储在方法栈帧中的局部变量中的数据。 这样,在执行一个操作码时,除了可以使用紧随其后的操作数,虚拟机还可以使用操作数栈中的数据,或局部变量中的数据,或者两者都用。
字节码检查器要进行大量的检查,以确保采用任何路径在字节码流中都得到一个确定的操作码,确保操作数栈总是包含正确的数值以及正确的类型。它必须保证局部变量在赋予合适的值以前都不能被访问,而且类的字段中必须总是被赋予正确类型的值,类的方法被调用时总是传递正确的数值和类型的参数。字节码还必须保证每个操作码都是合法的,即每一个操作码都有合法的操作数,以及对每一个操作码,合适类型的数值位于局部变量中或是在操作数栈中。这些仅仅是字节码检验器所做的大量检查工作中的一个小部分,在整个检验过程中,它就能保证这个字节码流可以Java虚拟机安全的执行。
第四趟 验证符号引用的正确性:即追踪那些符号引用, 以确保引用的正确性。 这个过程可能需要载入新的类。 大多数Java虚拟机的实现采用延迟装载类的策略,直到类真正地被程序使用时才装载。 也就是说事实上第四趟可能不会紧随着第三趟进行。
Java虚拟机内置的安全特性
Java虚拟机在执行字节码时还进行一些内置的安全机制的操作,这些机制大多数十Java类型安全的基础,这些机制有:类型安全的引用转换, 结构化的内存访问, 自动垃圾收集, 数组边界检查, 空引用检查。
非结构化内存访问的一个典型的例子就是指针,指针可以随便偏移,这非常容易引起崩溃。 通过保证一个Java程序只能使用类型安全的、结构化的方法去访问内存,Java虚拟机使得Java程序更为健壮,可以阻挠那些黑客,是他们不能为了达到某些目的而破坏Java虚拟机的内在存储,也使得它们运行更为安全。
作为内存结构化访问的一个后备,Java虚拟机并未指明运行时数据空间在Java虚拟机内部是怎么分布的。运行时数据空间是指一些内存空间,Java虚拟机用这些空间来存储运行一个Java程序时所需要的数据:Java栈,一个存储字节码的方法区,以及一个垃圾收集堆(它用来存储由运行的程序创建的对象)。 通过class文件探测不到任何内存地址。
虽然字节码指令集没有向用户提供不安全的,非结构化的内存方法,但是可以绕过字节码,即调用本地方法。在调用本地方法时,Java安全沙箱完全不起作用。
为了保证安全而内置的Java虚拟机的最后一个机制,就是异常的机构化错误处理,因为Java虚拟机支持异常,所以当一些违反安全的行为发生时,它会做一些结构化的处理,Java虚拟机将抛出一个异常或者一个错误,而不是崩溃,这个异常或者错误将导致这个错误线程的死亡,而不是使整个系统陷入瘫痪。(抛出一个错误总是导致抛出错误的线程死亡)
安全管理器和JavaAPI
它主要用于保护虚拟机的外部资源不被虚拟机内运行的恶意或者有漏洞的代码侵犯,这个安全管理器是一个单独的对象,它在访问控制—对于外部资源的访问控制—中起中枢作用。当一个JavaAPI即将进行一个潜在不安全的动作时,它将遵循以下两个步骤:首先,JavaAPI的代码检查有没有安装安全管理器,如果没有安装,则跳过第二步直接执行这个潜在不安全操作。否则,在第二步中,它将调用安全管理器中的合适的“check”方法,如果这个操作被禁止,那么这个”check”方法会抛出一个安全异常,这将导致JavaAPI方法立即中止,这个潜在不安全的操作将不会被执行。