Java安全模型侧重于保护终端用户免受从网络下载的、来自不可靠来源的、恶意程序(以及善意程序中的bug)的侵犯。为了达到这个目的,Java提供了一个用户可配置的“沙箱”,在沙箱中可以放置不可靠的Java程序。
2.1 基本沙箱
沙箱模型使你可以接收来自任何来源的代码,而不是要求用户避免将来自不受信站点的代码下载到机器上。
组成Java沙箱的基本组件如下:
- 类装载器结构
- class文件检验器
- 内置于Java虚拟机(及语言)的安全特性
- 安全管理器及Java API
2.2 类装载器体系结构
在Java沙箱中,类装载器是第一道防线:
- 它防止恶意代码去干涉善意的代码
- 它守护了被信任的类库的边界
- 它将代码归入某类(称为保护域),该类确定了代码可以进行哪些操作
类装载器体系结构可以防止恶意的代码去干涉善意的代码,是通过为由不同的类装载器装入的类提供不同的命名空间来实现的。命名空间由一系列唯一的名称组成,每一个被装载的类有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的。
类装载器体系结构守护了被信任的类库的边界,是通过分别使用不同的类装载器装载可靠的包和不可靠的包来实现的。
从Java 1.2版本开始,类装载器请求另一个用户自定义类装载器来装载一个类型的过程被形式化,称为双亲委派模式。除启动类装载器外的每一个类装载器,都有一个“双亲”类装载器,在某个特定的类装载器试图以常用方式装载类型之前,它会先默认地将这个任务“委派”给它的双亲——请求它的双亲来装载这个类型。
运行时包,是指由同一个类装载器装载的、属于同一个包的、多个类型的集合。在允许两个类型之间对包内可见的成员进行访问前,虚拟机不但要确定这两个类型属于同一个包,还必须确认它们属于同一个运行时包——它们必须是由同一个类装载器装载的。
除了屏蔽不同命名空间的类以及保护受信任的类库的边界外,类装载器还起到了另外的安全作用。类装载器必须将每一个被装载的类放置在一个保护域中,一个保护域定义了这个代码在运行时将得到怎样的权限。
2.3 class文件检验器
和类装载器一起,class文件检验器保证装载的class文件内容有正确的内部结构,并且这些class文件相互间协调一致。如果class文件检验器在class文件中发现了问题,它会抛出异常。
JVM的class文件检验器只在字节码执行之前进行一次分析,需要进行四趟独立的扫描:
- 第一趟是在类被装载时进行的,主要检查class文件的内部结构
- 第二趟和第三趟是在连接过程中进行的,主要确认类型数据遵从Java编程语言的语义,包括检验它所包含的所有字节码的完整性。
- 第四趟是在动态连接的过程中解析符号时进行的,主要确认被引用的类、字段以及方法确实存在。
2.4 JVM中内置的安全特性
JVM在执行字节码时还进行其他一些内置的安全机制的操作,包括:
- 类型安全的引用转换
- 结构化的内存访问(无指针算法)
- 自动垃圾收集(不必显式地释放被分配的内存)
- 数组边界检查
- 空引用检查
2.5 安全管理器和Java API
Java安全模型的前三个组成部分——类装载器体系结构、class文件检验器以及Java中内置的安全特性——一起达到一个共同的目的:保持JVM的实例和它正在运行的应用程序的内部完整性,使得它们不被下载的恶意或有漏洞的代码侵犯。相反,这个安全模型的第四个组成部分是安全管理器,它主要用于保护虚拟机的外部资源不被虚拟机内运行的恶意或有漏洞的代码侵犯。
安全管理器定义了沙箱的外部边界。因为它是可定制的,所以它允许为程序建立自定义的安全策略。
Java应用程序在启动时不会默认安装安全管理器,也不会有任何安全限制。在Java 1.2版本以前,如果安装了安全管理器,那么它将负责应用程序整个剩余的生命周期,不能被替代、扩展或者修改。在1.2版本中,当前安装的安全管理器能够在允许的情况下被替换。
安全管理器主要负责两个方面的工作:说明一个安全策略以及执行这个安全策略。
Java 1.2版本中的java.lang.SecurityManager
是一个具体的类,它提供了一个默认的安全管理器的实现,能够辅助建立一个基于代码签名的细粒度的安全策略。用户可以显示实例化这个安全管理器,或者让它自动安装。例如,在Sun的Java 2 SDK版本1.2中,可以在命令行使用-Djava.security.manager
选项来指明安装具体安全管理器。
具体安全管理器类允许用户不用Java代码定义自己的安全策略,而是用一个称为策略文件的ASCII文件。在策略文件中,可以给代码来源授予权限。权限是用类定义的,它是java.security.Permission
的子类。代码来源是由代码库的URL和一些签名组成的,从这个URL可以装载代码,而签名则为这个代码作担保。当创建安全管理器时,它对策略文件进行解析,并创建CodeSource(代码来源)和Permission(权限)对象,这些对象被封装在一个单独的Policy对象中,这个Policy对象就代表了运行时的策略。任何时刻只能有一个Policy对象被安装。
类装载器将类型放到保护域(ProtectionDomain)中,保护域封装了授予代码来源的所有权限,这些代码来源由装载的类型代表。
当具体安全管理器的check方法被调用时,它们中的大多数都将请求传递给一个称为AcessController的类。这个AccessController使用了包含在保护域对象中的信息,这个对象所属的类的方法在调用栈中,AccessController进行栈检查以确定这个操作能否被执行。
在 java.lang.SecurityManager
中有两个关键方法:
- checkPermission(Permission perm) —— 进行某个操作(它需要指定的权限)前被调用
- checkPermission(Permission perm, Object context) —— 在被传递的安全上下文中进行某个操作(它需要指定的权限)前被调用
在具体安全管理器类中,checkPermission( )方法同样负责决定,是否允许将某个操作的任务委派给另一个方法。这个checkPermission( )方法只是简单地调用了类java.security.AccessController
中的静态checkPermission( )方法,并将这个Permission对象传递给它。
2.6 代码签名和认证
Java安全模型很重要的一点就是它支持认证。认证可以使用户确认,由某些团体担保的一些class文件是值得信任的,并且这些class文件在到达用户虚拟机的途中没有被改变。这样,如果用户在一定程度上信任这个为代码作担保的团体,也就可以在一定程度上简化沙箱对这段代码的限制。可以对由不同团体签名的代码建立不同的安全限制。
2.7 策略
Java安全体系结构的真正好处在于,它可以对代码授予不同层次的信任度来部分地访问系统。
版本1.2的安全体系结构的主要目标之一就是使建立(以代码签名为基础的)细粒度的访问控制策略的过程更为简单且更少出错。在版本1.2的安全模型中,权限(系统访问权限)使授予代码来源的。
对应于整个Java应用程序的一个访问控制策略是由抽象类java.security.Policy的一个子类的单个实例所表示的。在任何时候,每个应用程序实际上都只有一个Policy对象。类装在其利用这个Policy对象来帮助它们决定,在把一段代码导入虚拟机时应该给它们怎么样的权限。
安全策略是一个从描述运行代码的属性集合到这段代码所拥有的权限的映射。在版本1.2的安全体系结构中,描述运行代码的属性被总称为代码来源。
在版本1.2中,所有和具体安全管理器有关的工具和访问控制体系结构都只对证书起作用,而不能对“赤裸”的公钥起作用。
权限是由抽象类java.security.Permission
的一个子类的实例表示的。一个Permission对象有三个属性:类型、名字和可选的操作。
在Policy对象中,每一个CodeSource是和一个或多个Permission对象相关联的。
策略文件
由Sun提供的在Java 1.2平台下的具体Policy子类采用如下方法:在一个ASCII策略文件中用上下文无关文法描述安全策略。
keystore "ijvmkeys";
grant signedBy "friend" {
permission java.io.FilePermission "question.txt", "read";
permission java.io.FilePermission "answer.txt", "read";
};
grant signedBy "stranger" {
permission java.io.FilePermission "question.txt", "read";
};
grant codeBase "file:${com.artima.ijvm.cdrom.home}/security/ex2/*" {
permission java.io.FilePermission "question.txt", "read";
permission java.io.FilePermission "answer.txt", "read";
};
最后一条grant语句中的代码库URL采用了文件的形式:它包含了一个属性${com.artima.ijvm.cdrom.home}
。
2.8 保护域
当类装载器将类型装入JVM时,它们将为每个类型指派一个保护域。保护域定义了授予一段特定代码的所有权限。(一个保护域对应于策略文件中的一个或多个grant子句。)装载入JVM的每一个类型都属于且仅属于一个保护域。
虽然前面的Policy对象代表了一个从代码来源到权限的全局映射,但是最终还是由类装载器负责决定代码执行时将获得怎样的权限。
图2-4用图形化的方式描述了保护域、代码来源以及权限。ProtectionDomain对象封装了一个到CodeSource对象的引用以及一个到java.security.Permissions
对象的引用。java.security.Permissions
是抽象类java.security.PermissionCollection
的一个具体类,代表了一个同构权限的集合。
当一个类装载器将Friend和Friend$1
导入方法区时,类装载器将把一个ProtectionDomain对象的引用和这些class文件的字节传递给defineClass( )方法。这个defineClass( )方法将Friend和Friend$1
所在的方法区中的类型数据和被传递的ProtectionDomain对象相关联。
2.9 访问控制器
类java.security.AccessController
提供了一个默认的安全策略执行机制,它使用栈检查来决定潜在不安全的操作是否被允许。
由AccessController的checkPermission( )实现的基本算法决定了调用栈中的每个帧是否有权执行潜在不安全的操作。每一个栈帧代表了由当前线程调用的某个方法,每一个方法是在某个类中定义的,每一个类又属于某个保护域,每个保护域包含一些权限。因此,每个栈帧间接地和一些权限相关。
当调用doPrivileged( )方法时,就像调用其它任何方法一样,都会将一个新的栈帧压入栈。在由AccessController执行的栈检查中,一个doPrivileged( )方法调用的栈帧标识了检查过程的提前终止点。如果和调用doPrivileged( )的方法相关联的保护域拥有执行被请求操作的权限,AccessController将立即返回。这样这个操作就被允许,即使在栈下层的代码可能没有执行这个操作的权限。