Java 中的安全模型
在 Java 中将执行程序分成本地和远程两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的 远程代码在早期的 Java 实现中,安全依赖于沙箱 (Sandbox) 机制。沙箱机制就是将 Java 代码限定在虚拟机 (JVM) 特定的运行范围中,并且严格限制代码对本地系统的资源访问,通过这样的措施来保证对远程代码的有效隔离,防止对本地系统造成破坏。如图 1 所示,
图 1.JDK1.0 安全模型
但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。因此在后续的 Java1.1 版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限。如图 2 所示,
图 2.JDK1.1 安全模型
在 Java1.2 版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。如图 3 所示,
图 3.JDK1.2 安全模型
当 前最新的安全机制实现,则引入了域 (Domain) 的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的 资源进行访问。虚拟机中不同的受保护域 (Protected Domain),对应不一样的权限 (Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如图 4 所示:
图 4. 最新安全模型
以 上提到的都是基本的 Java 安全模型概念,在应用开发中还有一些关于安全的复杂用法,其中最常用到的 API 就是 doPrivileged。doPrivileged 方法能够使一段受信任代码获得更大的权限,甚至比调用它的应用程序还要多,可做到临时访问更多的资源。有时候这是非常必要的,可以应付一些特殊的应用场 景。例如,应用程序可能无法直接访问某些系统资源,但这样的应用程序必须得到这些资源才能够完成功能。针对这种情况,Java SDK 给域提供了 doPrivileged 方法,让程序突破当前域权限限制,临时扩大访问权限。下面内容会详细讲解一下安全相关的方法使用。
Java 安全控制实现
Java SDK 中与安全相关的类和接口都放在 java.security 包中,其中既包括访问控制配置及细粒度访问控制框架的实现,还包括签名和加解密的实现。本文中涉及到的安全访问控制主要与安全包中访问控制框架相关,这里 面最常用的就是 AccessContorller 类。通过下图的描述,您可以了解 ACC(Access Contorller) 机制是如何运作的。
在某一个线程的调用栈中,当 AccessController 的 checkPermission 方法被最近的调用程序(例如 A 类中的方法)调用时,对于程序要求的所有访问权限,ACC 决定是否授权的基本算法如下:
1. 如果调用链中的某个调用程序没有所需的权限,将抛出 AccessControlException;
2. 若是满足以下情况即被授予权限:
a. 调用程序访问另一个有该权限域里程序的方法,并且此方法标记为有访问“特权”;
b. 调用程序所调用(直接或间接)的后续对象都有上述权限。
在 上面例子的调用链中,假定 E 域和 F 域不具备 X 权限 (permission),而在 C.class 对应的 G 域具有 X 权限,同时 C 使用 X 权限的对外接口 Y 方法是通过 doPrivilege 方式实现。那么,B.class A.class 调用 Y 方法就都具备 X 权限。如果 Y 方法没有标注 doPrivilege,那么对 Y 方法的调用就不具备 X 权限。
还有一种特殊的情况,就是访问控制上 下文的继承问题。当一个线程创建另一个新线程时,会同时创建新的堆栈。如果创建新线程时没有保留当前的安全上下文,也就是线程相关的安全信息,则新线程调 用 AccessController.checkPermission 检验权限时,安全访问控制机制只会根据新线程的上下文来决定安全性问题,而不会考虑其父线程的相应权限。这个清除堆栈的做法本身并不会给系统带来安全隐 患,但它会使源代码,尤其是系统代码的编写容易出现错误。例如,对安全框架实现不熟悉编程人员可能会很自然地认为,子线程执行的信任代码继承了父线程执行 的不可信任代码的安全限制特性。当从子线程内访问受控制的资源时,如果父线程的安全上下文信息并未保存,就会导致意外的安全漏洞。因为丢失的父线程中安全 限制数据会使子线程将资源传递给一些不可信任的代码。因此,在创建新线程时,必须确保利用父线程创建,或利用其他形式创建代码。总之,要保证让子线程自动 继承父线程的安全性上下文,这样子线程中的后续 AccessController.checkPermission 调用就会考虑所继承的父线程的安全特性。
需要注意是 AccessController 类的 checkPermission 方法将在当前执行线程的上下文,包括继承的上下文中进行安全检查。当这种安全检查只能在不同的上下文中进行时就会出现问题。意即,本应在一个线程上下文内 部进行的安全检查,有时却需要在不同上下文中进行。例如,当一个线程将某个事件传给另一个线程时,如果所请求的事件服务要求访问某种安全受控资源,则为其 请求事件服务的第二个线程将没有事件产生源线程相应的上下文来完成所需的访问控制决策。为解决这样的问题,Java 在 AccessController 类中提供了 getContext 方法和 AccessControlContext 对象。通过 getContext 方法可获取当前调用上下文的“快照 (snapshot)”,然后将其存放到返回的 AccessControlContext 对象中。调用的样例程序如下所示:AccessControlContext acc = AccessController.getContext();
getContext 方法将当前上下文的快照信息捕获,然后执行程序就可以通过检查前后不同上下文中的信息,即比较快照上下文信息与本上下文信息,然后来做出对受控资源访问控 制的决策。上面问题就可以如下方式来解决,当前一个线程把某个请求事件传给第二个线程时,同时捕获其上下文信息并将这些信息提供给后一个线程。略有不同的 是,AccessControlContext 类本身的 checkPermission 方法可根据它自身携带的上下文信息来决定访问控制,而不是根据当前正在执行的线程上下文。因此必要时,后一个线程可直接通过调用前一个线程上下文快照本身 的权限检查方法来执行相应的安全检查。如下,acc.checkPermission(permission),上述方法调用等同于在前一个线程的上下文 中执行相同的安全检查,尽管访问控制检查实际上是在后一个线程中完成的。
安全控制使用的代码实例
这里没有使用原文提供的代码而是自己进行了测试,使用的环境是Ubuntu14.04.01,IDE为MyEclipse.
工作空间路径为
/home/chenyi/java/Workspaces/MyEclipse
新建一个JAVA项目securityA
这个项目中提供两个静态方法,分别是普通方式在本地创建文件和特权访问方式在本地创建文件。
package demo;import java.io.File;import java.io.IOException;import java.security.AccessControlException;import java.security.AccessController;import java.security.PrivilegedAction;public class FileUtil { // 工程 A 执行文件的路径 private final static String FOLDER_PATH = "/home/chenyi/java/Workspaces/MyEclipse/securityA/bin"; public static void makeFile(String fileName) { try { // 尝试在工程 A 执行文件的路径中创建一个新文件 File fs = new File(FOLDER_PATH + "/" + fileName); fs.createNewFile(); } catch (AccessControlException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } public static void doPrivilegedAction(final String fileName) { // 用特权访问方式创建文件 AccessController.doPrivileged(new PrivilegedAction() { @Override public String run() { makeFile(fileName); return null; } }); } public static void main(String[] args){ FileUtil.makeFile("FileUtil类正常方式文件创建测试"); FileUtil.doPrivilegedAction("FileUtil类特权方式文件创建测试"); } }
测试两个静态方法是否可用。
可以在/home/chenyi/java/Workspaces/MyEclipse/securityA/bin 下看到成功生成的两个文件"FileUtil类正常方式文件创建测试" 和 "FileUtil类特权方式文件创建测试"
再次新建一个JAVA项目securityB
首先建立securityB对securityA的引用
通过Build Path --> Projects --> Add --> securityA 建立对项目securityA的引用
package securityB.demo;import java.io.File;import java.io.IOException;import java.security.AccessControlException;import demo.FileUtil;public class FileText { public static void main(String[] args){ //加载安全访问策略文件 System.setProperty("java.security.policy", "MyPolicy.policy"); //打开系统安全权限检查 System.setSecurityManager(new SecurityManager()); System.out.println("-------------------------------------------------------------------------------------------------"); System.out.println("FileText类调用FileUtil的特权方式文件创建方法测试"); FileUtil.doPrivilegedAction("FileText类调用FileUtil的特权方式文件创建方法测试"); System.out.println("-------------------------------------------------------------------------------------------------"); System.out.println("FileText类调用FileUtil的正常方式文件创建方法测试"); FileUtil.makeFile("FileText类调用FileUtil的正常方式文件创建方法测试"); System.out.println("-------------------------------------------------------------------------------------------------"); System.out.println("FileText类直接在FileUtil的工程目录下以普通方式建立"); try { //FileText类直接在FileUtil的工程目录下以普通方式建立 File fs = new File( "/home/chenyi/java/Workspaces/MyEclipse/securityA/bin/FileText类直接在FileUtil的工程目录下以普通方式建立"); fs.createNewFile(); } catch (IOException e) { e.printStackTrace(); } catch (AccessControlException e1) { e1.printStackTrace(); } }}
如果没有
//打开系统安全权限检查 System.setSecurityManager(new SecurityManager());
文件将会全部创建成功。执行结果。
-------------------------------------------------------------------------------------------------FileText类调用FileUtil的特权方式文件创建方法测试-------------------------------------------------------------------------------------------------FileText类调用FileUtil的正常方式文件创建方法测试-------------------------------------------------------------------------------------------------FileText类直接在FileUtil的工程目录下以普通方式建立
如果打开了系统安全权限检查,但是没有
//加载安全访问策略文件 System.setProperty("java.security.policy", "MyPolicy.policy");
运行时
执行结果如下:
-------------------------------------------------------------------------------------------------FileText类调用FileUtil的特权方式文件创建方法测试java.security.AccessControlException: access denied ("java.io.FilePermission" "/home/chenyi/java/Workspaces/MyEclipse/securityA/bin/FileText类调用FileUtil的特权方式文件创建方法测试" "write") at java.security.AccessControlContext.checkPermission(AccessControlContext.java:372) at java.security.AccessController.checkPermission(AccessController.java:559) at java.lang.SecurityManager.checkPermission(SecurityManager.java:549) at java.lang.SecurityManager.checkWrite(SecurityManager.java:979) at java.io.File.createNewFile(File.java:1002) at demo.FileUtil.makeFile(FileUtil.java:17) at demo.FileUtil$1.run(FileUtil.java:30) at demo.FileUtil$1.run(FileUtil.java:1) at java.security.AccessController.doPrivileged(Native Method) at demo.FileUtil.doPrivilegedAction(FileUtil.java:27) at securityB.demo.FileText.main(FileText.java:20)-------------------------------------------------------------------------------------------------FileText类调用FileUtil的正常方式文件创建方法测试java.security.AccessControlException: access denied ("java.io.FilePermission" "/home/chenyi/java/Workspaces/MyEclipse/securityA/bin/FileText类调用FileUtil的正常方式文件创建方法测试" "write") at java.security.AccessControlContext.checkPermission(AccessControlContext.java:372) at java.security.AccessController.checkPermission(AccessController.java:559) at java.lang.SecurityManager.checkPermission(SecurityManager.java:549) at java.lang.SecurityManager.checkWrite(SecurityManager.java:979) at java.io.File.createNewFile(File.java:1002) at demo.FileUtil.makeFile(FileUtil.java:17) at securityB.demo.FileText.main(FileText.java:24)-------------------------------------------------------------------------------------------------FileText类直接在FileUtil的工程目录下以普通方式建立java.security.AccessControlException: access denied ("java.io.FilePermission" "/home/chenyi/java/Workspaces/MyEclipse/securityA/bin/FileText类直接在FileUtil的工程目录下以普通方式建立" "write") at java.security.AccessControlContext.checkPermission(AccessControlContext.java:372) at java.security.AccessController.checkPermission(AccessController.java:559) at java.lang.SecurityManager.checkPermission(SecurityManager.java:549) at java.lang.SecurityManager.checkWrite(SecurityManager.java:979) at java.io.File.createNewFile(File.java:1002) at securityB.demo.FileText.main(FileText.java:33)
发现任何方法都无法创建文件,即此时项目securityA也失去了权限
而添加了安全访问控制策略文件 (MyPolicy.txt)之后,文件内容如下,放置在securityB
// 授权工程 A 执行文件路径中文件在本目录中的写文件权限 grant codebase "file:/D:/workspace/projectX/bin" { permission java.io.FilePermission "D:\\workspace\\projectX\\bin\\*", "write"; };
-------------------------------------------------------------------------------------------------FileText类调用FileUtil的特权方式文件创建方法测试-------------------------------------------------------------------------------------------------FileText类调用FileUtil的正常方式文件创建方法测试java.security.AccessControlException: access denied ("java.io.FilePermission" "/home/chenyi/java/Workspaces/MyEclipse/securityA/bin/FileText类调用FileUtil的正常方式文件创建方法测试" "write") at java.security.AccessControlContext.checkPermission(AccessControlContext.java:372) at java.security.AccessController.checkPermission(AccessController.java:559) at java.lang.SecurityManager.checkPermission(SecurityManager.java:549) at java.lang.SecurityManager.checkWrite(SecurityManager.java:979) at java.io.File.createNewFile(File.java:1002) at demo.FileUtil.makeFile(FileUtil.java:17) at securityB.demo.FileText.main(FileText.java:26)-------------------------------------------------------------------------------------------------FileText类直接在FileUtil的工程目录下以普通方式建立java.security.AccessControlException: access denied ("java.io.FilePermission" "/home/chenyi/java/Workspaces/MyEclipse/securityA/bin/FileText类直接在FileUtil的工程目录下以普通方式建立" "write") at java.security.AccessControlContext.checkPermission(AccessControlContext.java:372) at java.security.AccessController.checkPermission(AccessController.java:559) at java.lang.SecurityManager.checkPermission(SecurityManager.java:549) at java.lang.SecurityManager.checkWrite(SecurityManager.java:979) at java.io.File.createNewFile(File.java:1002) at securityB.demo.FileText.main(FileText.java:35)
只有调用在FileUtil的特权方法创建成功
注意,如果加载安全访问策略文件和打开安全检查的顺序相反,则会
Exception in thread "main" java.security.AccessControlException: access denied ("java.util.PropertyPermission" "java.security.policy" "write") at java.security.AccessControlContext.checkPermission(AccessControlContext.java:372) at java.security.AccessController.checkPermission(AccessController.java:559) at java.lang.SecurityManager.checkPermission(SecurityManager.java:549) at java.lang.System.setProperty(System.java:783) at securityB.demo.FileText.main(FileText.java:16)