学习了一下Java的安全机制,以前学习C++的时候好像就从来没有考虑过太多安全方面的问题,一些代码方面的安全问题,诸如指针、内存什么的考虑过,但是整体的安全性基本无视,学习了这一章还是有蛮多收获。
沙箱
组成沙箱的四个组件:
- 类装载器
- class文件检验器
- Java虚拟机内置的安全特性
- 安全管理器
类装载器
通过命名空间隔离类,使不同命名空间的类不会互相访问(显示指定了访问方式的例外),解决了类的访问范围问题,如下图:
类的加载(装载)顺序问题和防止恶意加载的方法,先看下图:
上图中的网络类装载器理解为用户自定义的装载器,启动类装载器是Java的顶级类装载器(顶级表示层次,不是性能、品味、奢侈等)和标准扩展类装载器理解为系统自带的类装载器。
例,当需要加载类A的时候,会先从父装载器开始寻找是否已经存在类A,如果没有,迭代寻找父装载器,直到启动类装载器返回结果,如果找到了,就由父装载器加载,如果都没有,则由自己加载。
类装载器加载类有一些保护规则,如下:
- 使用父装载器优先加载类的方式,能够防止系统类被替换。例,从网络上加载java.land.Integer将会被拒绝,因为由启动类装载器加载的Java API中已经存在,所以原Integer不会被替换。
- 只有由同一个类装载器加载的且同一个包内的类才可以互相访问。例,一个自己写的病毒类java.lang.Virus,由于系统中不存在该类,所以上面的约束1无效,由网络类装载器加载成功,而且与java.lang.*类在同一个包,但是由于类装载器不相同,所以Virus也无法访问java.lang.*。
- 增加黑名单。类装载器可以指定禁止加载的类列表,如果发现企图加载,则立即抛出异常。
class文件检验器
安全性开始进入到了类的内部了。Java虚拟机对class文件有4次检查:
- class文件的结构检查。在类被加载时进行。文件格式检查,检查包头、版本、文件长度等。
- 类型数据的语义检查。在连接过程中进行。class文件检验器在运行时检查了一些Java语言在编译时应该遵守的强制规则。
- 字节码验证。在连接过程中进行。我的理解就是检查方法,对方法(操作码、操作数)进行检查,确保能够正确访问。由于“停机问题”的缘故,检查的规则是一个特定的规则集合,而不是所有可能的情况。
- 符号引用的验证。这个是在动态连接的过程中,对外部调用的检查。例,NoClassDefFoundError并不是在预加载过程弹出的,而是直到这个不存在的类首次被程序使用的时候才弹出。顺便提一下,动态连接时,查找被引用的类(如果需要就加载该类),把符号引用转换为直接引用(指针、偏移量)。
二进制兼容问题,为什么要在动态连接时进行检查(上面第四条),一个原因是防止原有的class发生了变化,有专门的二进制兼容规则描述哪些情况是可以兼容的。例如,在被调用的类中新添加方法是可以的,但是给老方法改名是不可以的。我个人的理解就是:以符号引用的方法进行动态连接,只要符号不变,就OK,符号变了就不行。
Java虚拟机内置的安全特性
重复一下,之前也有了解,多写一下,权当是背诵了。
- 类型安全的引用转换(强制显示类型转换)
- 结构化内存访问(无指针)
- 自动垃圾回收
- 数组边界检查
- 空引用检查
下面是以前不太了解的:
- 不指明运行时数据空间分布。Java栈(每个线程一个)、方法区、垃圾收集堆的数据都不预先指明内存地址,只在运行时由Java虚拟机分配。
- 本地方法不受Java虚拟机安全性的限制。只要开始运行本地方法,就跳出了Java虚拟机的沙箱了。比较安全的做法,是不要直接调用本地方法,通过Java API进行调用,这样可以利用Java API本身的安全性来控制。
- 线程死亡不影响其它线程。
安全管理器
每个Java应用程序只有一个Policy对象,通过java.security.Policy.setPolicy()可以使用新的Policy对象替换当前的Policy对象,类装载器利用这个Policy对象来帮助它决定在把一段代码导入虚拟机时给它们赋予什么样的权限,另外,类装载器也可以自行添加其它的权限或者无视Policy的权限。
关于代码源(CodeSource)、签名和保护域,只用下图来稍作解释:
上图示例是Friend类经过编译后为Friend.class和Friend$1.class两个文件,打包为friend.jar,在方法区中为Friend和Friend$1(这个我现在还不太明白)。需要限制它对question.txt和answer.txt两个文件的权限。
- CodeSource由唯一的代码URL和签名[0…N]组成,上图中是文件系统中的friend.jar和friend签名组成,签名可以没有。
- 文件的权限是权限的一种,一个权限(Permission)表示为类型、属性和动作。如FilePermission、“question.txt”、“read”表示对question.txt的文件权限为读。
- 多个权限组成PermissionCollection,图中表示为Permissions。
- 保护域(ProtectionDomain)就是将CodeSource与多个权限关联起来。
Policy对象有一个getPermission的方法,通过传入一个CodeSource,可以获得其对应的Permissions。
访问控制器
java.security.AccessController提供了一个默认的安全策略执行机制,使用栈检查方式来决定操作是否被允许。这个类不能被实例化,它只包含多个静态方法。
核心函数是void checkPermission(Permission),如果允许,则简单返回,如果不允许,则抛出AccessControllerException或其子类。
老版本采用的是直接的函数调用,在Java1.2版本中将老版本的函数简单化为:实例化为对应的Permission,调用checkPermission函数。
Permission、PermissionCollection和ProtectionDomain都有一个implies(Permission)方法,Permission中的implies的作用是看自身的权限是否包含了参数中Permission的权限,PermissionCollection和ProtectionDomain的implies的作用是看自身集合的权限是否包了参数中Permission的权限。
权限的栈检查,检查到没有权限就抛出错误,否则正常返回。
doPrivilege()方法将终止栈检查来获得检查的方便性。检查执行到doPrivilege的下一步停止。如果doPrivilege的权限小于自身的权限,则自身的权限会被降低到doPrivilege中提供的权限。
其它
Java不能解决的是内存一直增长,线程太多可能会很慢,没有用户和权限的映射(如Unix,不知道现在有没有)。
安全还有其它方面的问题,比如物理上的,有人把东西偷了,你的员工是间谍等等。
心得
继续有收获,之前对安全方面的了解真的很少,现在算是初步有点了解,后续还需要多加深。