@changedi
2016-06-14T09:45:24.000000Z
字数 2669
阅读 2461
Java
安全
Java语言本身的安全机制是要保护内存资源——保证内存完整性,核心的安全特性要确保程序不能非法解析或修改驻留在内存中的机密信息。从语言本身的设计角度考虑,就是要设计一组规则,在所构建的运行环境中,程序对象对内存的操作是经过定义的而不是任意的。
ClassCastException
。Java语言通过上面几条约束,从语言层面保护了内存,不会允许程序在没有获得正确的访问权限时读取到本不该访问的内存。
这个题目不太好,因为序列化无法保证安全。Java允许通过实现java.io.Serializable
接口来使内存对象序列化为一组字节码。这组字节码通过网络或者文件等方式被其他地方的代码读取并重建一个相同结构和内容的内存对象。这是Java的序列化和反序列化过程。作为一组字节码存储在磁盘文件或者数据流里,原则上是允许被修改的。那说白了,序列化无法保障安全。但是考虑到Java语言要设计支持一组规则,至于序列化的安全,就交给使用者自己保障。
序列化的安全设计规则:
1. 可序列化的对象必须实现java.io.Serializable
接口,这相当于给用户打预防针,告知其要考虑这个对象的安全问题。
2. 序列化对象声明transient
变量,那么该变量不被序列化。这相当于提供了保护数据的机制。
单单从这两个层面看,已经是Java语言能做到的最大化问题了。再多,则影响到了序列化本身要实现的功能。那么具体怎么做呢?就像我们常规传输数据一样——加密,你可以选择对要序列化的变量和属性加密,在反序列化时解密来增强安全。
当然这里讲到的实施其实不仅仅针对安全,但是这些实施阶段确实增强了安全性。换个角度,我们其实是看Java程序的运行过程如何对应这些语言设计规则。
编译阶段,可以避免“约束”中提到的前4条规则。数组越界是运行时问题,而类型转换,在编译阶段只能做到无关类型的转换,比如下例:
static class Foo {
int x;
}
static class Bar {
int x;
}
public static void main(String[] args) {
Foo foo = new Foo();
Bar bar = (Bar) foo;
}
这时编译器会提示“Cannot cast from Foo to Bar”。但是如果稍作修改,将Foo替换为Object,则编译器无能为力。
static class Foo {
int x;
}
static class Bar {
int x;
}
public static void main(String[] args) {
Object foo = new Foo();
Bar bar = (Bar) foo;
}
我们都知道,编译只是做Class文件,JVM的介入是要从load class开始的。而加载完class文件后的第一件事就是链接。链接包括验证、准备和解析这几个步骤,而验证阶段,就是安全规则介入的一个阶段。验证阶段就引入了JVM的字节码校验器。
这个阶段主要是来防御恶意编译器的攻击,或者是一些无意的程序错误。比如一个类FooBar设计如下:
public class FooBar {
public String val = "abcd";
}
类FooBarTest引用了这个类并更改了val变量:
public class FooBarTest {
public static void main(String[] args) {
FooBar foobar = new FooBar();
foobar.val = "abcde";
System.out.println(foobar.val);
}
}
这时我们编译并运行FooBarTest,会打印abcde。而现在如果去修复FooBar,将public改为private,然后只编译FooBar,则不会发生错误。然而本来这时也编译FooBarTest会导致编译错的。但是因为疏忽导致没有这么做,那么如果没有链接校验,FooBarTest就是错误的执行了。然而因为字节码校验器的存在,运行FooBarTest会抛出
Exception in thread "main" java.lang.IllegalAccessError: tried to access field FooBar.val from class FooBarTest
at FooBarTest.main(FooBarTest.java:5)
优雅的解决了这个问题。
字节码校验器通过两部分来实现这种校验。首先,其作为一个微型的定理证明机,会证明class满足下列条件(只做检查):
1. 类文件格式正确;
2. 不会基于final派生子类,也不会覆盖final方法;
3. 只有一个父类;
4. 没有对primitive类型数据进行非法转型(int->Object);
5. 对象之间没有进行类型转换;
6. 操作数栈不会出现溢出。
接下来,在代码真正执行前进行校验(称为延迟校验)。比如刚才举例中提到的异常,就是校验器在校验字段的访问合法性时抛出的。
上面两个阶段检查不了的规则,放到运行时检查:数组越界和类型转换。运行时抛出ArrayIndexOutOfBoundException
和ClassCastException
。
Java语言层面的安全设计,本身也可以看出设计思路是弥补原有C和C++的部分短板而设计的。主要目标还是防止非法内存访问。但是加入了这些限制也带来了性能的缺失,这本身就是一个trade off。