记录学习过程,整理笔记。
Java的安全模型是其多个重要结构特点之一,它使Java成为适用于网络环境的技术。Java安全模型侧重于保护终端用户免受从网络下载的、来自不可靠来源的、恶意程序(以及善意程序中的bug)的侵犯。
1、Java中的安全模型
Java 安全模型,你了解了吗
在Java中将执行程序分为本地和远程两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于受信的本地代码,可以访问一切本地资源。而对于非受信的远程代码在早期的Java实现中,安全依赖于沙箱(Sandbox)机制。沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统的资源访问,通过这样的措施来保证对远程代码的有效隔离,防止对本地系统造成破坏,如图1所示,
但如此严格的安全机制也给程序的功能扩展带来了障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。因此在后续的Java1.1版本中,针对安全机制做了改进,增进了安全策略,允许用户指定代码对本地资源的访问权限,如图2所示,
在Java 1.2版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。如图3所示,
当前最新的安全机制实现,则引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统与的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如图4所示:
以上提到的都是基本的Java安全模型概念,在应用开发中还有一些关于安全的复杂用法,其中最长用到的API就是doPrivileged。doPrivileged方法能够使一段受信任代码获得更大的权限,甚至比调用它的应用程序还要多,可做到临时访问更多的资源。有时候这是非常必要的,可以应付一些特殊的应用场景。例如应用程序可能无法直接访问某些系统资源,但这样的应用程序必须得到这些资源才能够完成功能。针对这种情况,Java SDK给域提供了doPrivileged方法,让程序突破当前域权限限制,临时扩大访问权限。
2、理解Java 沙箱
2.1 什么是沙箱
Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。限制程序运行一方面是为了保护系统资源,同时另一方面也为了保护程序自己。沙箱主要限制系统资源访问,那系统资源包括什么?---CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
所有的Java程序运行都可以指定沙箱,可以定制安全策略。而一个Java程序运行的安全策略,包括了以下几点基础:
- 字节码校验器(bytecode verifier):确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但是不是所有的类文件都会经过字节码校验,比如核心类。
- 类加载器(class loader):所有的java类都是通过类加载器加载的,可以自定义类加载器来设置加载类的权限。
- 存取控制器(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。
- 安全管理器(security manager):是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。
- 安全软件包(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
- 安全提供者
- 消息摘要
- 数字签名
- 加密
- 鉴别
2.2 沙箱包含哪些要素
2.2.1 权限
权限是指允许代码执行的操作。包含三部分:权限类型、权限名和允许的操作。权限名一般就是对哪类资源进行操作的资源定位(比如说一个文件名或者通配符、网络主机等),一般基于权限类型来设置,有的比如java.security.AllPermission不需要权限名。允许的操作也和权限类型对应,指定了都目标可以执行的操作行为,比如读、写等。如下面的例子:
permission java.security.AllPermission; //权限类型
permission java.lang.RuntimePermission "StopThread"; //权限类型+权限名
permission java.io.FilePermission "/tmp/foo" "read"; //权限类型+权限名+允许的操作
标准权限有哪些
| 类型 | 权限名 | 操作 | 例子 |
文件权限 | java.io.FilePermission | 文件名(平台依赖) | 读、写、删除、执行 | 允许所有问价的读写删除执行:permission java.io.FilePermission "<< ALL FILES>>", "read,write,delete,execute";。允许对用户主目录的读:permission java.io.FilePermission "${user.home}/-", "read";。 |
套接字权限 | java.net.SocketPermission | 主机名:端口 | 接收、监听、连接、解析 | 允许实现所有套接字操作:permission java.net.SocketPermission ":1-", "accept,listen,connect,resolve";。允许建立到特定网站的连接:permission java.net.SocketPermission ".abc.com:1-", "connect,resolve";。 |
属性权限 | java.util.PropertyPermission | 需要访问的jvm属性名 | 读、写 | 读标准Java属性:permission java.util.PropertyPermission "java.", "read";。在sdo包中创建属性:permission java.util.PropertyPermission "sdo.", "read,write";。 |
运行时权限 | java.lang.RuntimePermission | 多种权限名[见附录A] | 无 | 允许代码初始化打印任务:permission java.lang.RuntimePermission "queuePrintJob" |
AWT权限 | java.awt.AWTPermission | 6种权限名[见附录B] | 无 | 允许代码充分使用robot类:permission java.awt.AWTPermission "createRobot"; permission java.awt.AWTPermission "readDisplayPixels";。 |
网络权限 | java.net.NetPermission | 3种权限名[见附录C] | 无 | 允许安装流处理器:permission java.net.NetPermission "specifyStreamHandler";。 |
安全权限 | java.security.SecurityPermission | 多种权限名[见附录D] | 无 | |
序列化权限 | java.io.SerializablePermission | 2种权限名[见附录E] | 无 | |
反射权限 | java.lang.reflect.ReflectPermission | suppressAccessChecks(允许利用反射检查任意类的私有变量) | 无 | |
完全权限 | java.security.AllPermission | 无(拥有执行任何操作的权限) | 无 | |
2.2.2 代码源
代码源是类所在的位置,表示为以URL地址。
2.2.3 保护域
保护域用来组合代码源和权限,这是沙箱的基本概念。保护域就在于声明了比如由代码A可以做权限B这样的事情。
2.2.4 策略文件
策略文件在控制沙箱的管理元素,一个策略文件包含一个或多个保护域的项。策略文件完成了代码权限的指定任务,策略文件包含全局和用户专属两种。
为了管理沙箱,策略文件我认为是最重要的内容。JVM可以使用多个策略文件,不过一般两个最常用。一个是全局的:$JREHOME/lib/security/java.policy
,作用域JVM的所有实例。另一个是用户自己的,可以存储到用户的主目录下。策略文件可以使用jdk自带的policytool工具编辑。
默认的策略文件我们先参考一下:
// Standard extensions get all permissions by default
grant codeBase "file:${{java.ext.dirs}}/*" {
permission java.security.AllPermission;
};
// default permissions granted to all domains
grant {
// Allows any thread to stop itself using the java.lang.Thread.stop()
// method that takes no argument.
// Note that this permission is granted by default only to remain
// backwards compatible.
// It is strongly recommended that you either remove this permission
// from this policy file or further restrict it to code sources
// that you specify, because Thread.stop() is potentially unsafe.
// See the API specification of java.lang.Thread.stop() for more
// information.
permission java.lang.RuntimePermission "stopThread";
// allows anyone to listen on dynamic ports
permission java.net.SocketPermission "localhost:0", "listen";
// permission for standard RMI registry port
permission java.net.SocketPermission "localhost:1099", "listen";
// "standard" properies that can be read by anyone
permission java.util.PropertyPermission "java.version", "read";
permission java.util.PropertyPermission "java.vendor", "read";
permission java.util.PropertyPermission "java.vendor.url", "read";
permission java.util.PropertyPermission "java.class.version", "read";
permission java.util.PropertyPermission "os.name", "read";
permission java.util.PropertyPermission "os.version", "read";
permission java.util.PropertyPermission "os.arch", "read";
permission java.util.PropertyPermission "file.separator", "read";
permission java.util.PropertyPermission "path.separator", "read";
permission java.util.PropertyPermission "line.separator", "read";
permission java.util.PropertyPermission "java.specification.version", "read";
permission java.util.PropertyPermission "java.specification.vendor", "read";
permission java.util.PropertyPermission "java.specification.name", "read";
permission java.util.PropertyPermission "java.vm.specification.version", "read";
permission java.util.PropertyPermission "java.vm.specification.vendor", "read";
permission java.util.PropertyPermission "java.vm.specification.name", "read";
permission java.util.PropertyPermission "java.vm.version", "read";
permission java.util.PropertyPermission "java.vm.vendor", "read";
permission java.util.PropertyPermission "java.vm.name", "read";
};
策略文件的内容格式就是这样,grant授权允许操作某个权限。这个默认的策略文件就指明了jdk扩展包可以有全部权限,允许代码stop线程,允许监听1099端口等等。
另一个很重要的是参数文件——java.security
,这个文件和策略文件在同一个目录下。这个参数文件定义了沙箱的一些参数。比如默认的沙箱文件时这样的(截取部分):
# The default is to have a single system-wide policy file,
# and a policy file in the user's home directory.
policy.url.1=file:${java.home}/lib/security/java.policy
policy.url.2=file:${user.home}/.java.policy
# whether or not we expand properties in the policy file
# if this is set to false, properties (${...}) will not be expanded in policy
# files.
policy.expandProperties=true
# whether or not we allow an extra policy to be passed on the command line
# with -Djava.security.policy=somefile. Comment out this line to disable
# this feature.
policy.allowSystemProperty=true
policy.url.*这个属性指明了使用的策略文件,如上文所述,默认的两个位置就在这里配置,用户可以自行更改顺序和存储位置。而policy.allowSystemProperty指明是否允许用户自行通过命令行指定policy文件。
2.2.5 密钥库
保存密钥证书的地方。
2.3 默认沙箱
通过Java命令行启动的Java应用程序,默认不启用沙箱。要想启用沙箱,启动命令需要做如下形式的变更:
java -Djava.security.manager <other args>
沙箱启动后,安全管理器会使用两个默认的策略文件来确定沙箱启动参数。当然也可以通过命令指定:
java -Djava.security.policy=<URL>
如果要求自动时只遵循一个策略文件,那么启动参数要加个等号,如下:
java -Djava.security.policy==<URL>
2.4 如何使用
2.4.1 限制读文件
这个例子很简单,首先写一个r.txtx文件,里面的内容时“abcd”,再写个程序如下读取这个r.txtx。
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class PolicyTest {
public static void file() {
File f = new File("D:\\github\\CDLib\\src\\main\\resources\\security\\r.txt");
InputStream is;
try {
is = new FileInputStream(f);
byte[] content = new byte[1024];
while (is.read(content) != -1) {
System.out.println(new String(content));
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args) {
// test read file.
file();
}
}
发现输出时abcd。
接下来修改java启动参数,加入-Djava.security.manager
,启动了安全沙箱。再运行,输出变成了异常。
Exception in thread "main" java.security.AccessControlException: access denied ("java.io.FilePermission" "D:githubCDLibsrcmainresourcessecurityr.txt" "read")
at java.security.AccessControlContext.checkPermission(Unknown Source)
at java.security.AccessController.checkPermission(Unknown Source)
at java.lang.SecurityManager.checkPermission(Unknown Source)
at java.lang.SecurityManager.checkRead(Unknown Source)
at java.io.FileInputStream.<init>(Unknown Source)
at com.taobao.cd.security.PolicyTest.main(PolicyTest.java:15)
这里已经提示了,访问被拒绝,说明沙箱启动,同时也验证了默认沙箱---禁止本地文件访问。
再来,我们构建一个custom.policy文件如下:
grant {
permission java.io.FilePermission "D:\\github\\CDLib\\src\\main\\resources\\security\\*", "read";
};
这里构建了一条安全策略---允许读取security目录下的文件。
修改启动文件,添加-Djava.security.policy=D:\\github\\CDLib\\src\\main\\resources\\security\\custom.policy
,再执行,结果输出了abcd
。
如上例。我们通过自定义policy文件修改了默认沙箱的安全策略,再通过启动参数开启沙箱模式。这样就可以构造我们自己想要的沙箱效果了。
2.4.2 限制访问网络
通过HttpClient访问www.baidu.com
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import com.taobao.cd.http.util.HttpUtil;
public class PolicyTest {
public static void network() {
try {
String text = HttpUtil.createHtmlText("http://www.baidu.com", HttpUtil.UA);
System.out.println(text);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args) {
// test use network.
network();
}
}
开启沙箱后,输出如下:
java.security.AccessControlException: access denied ("java.net.SocketPermission" "www.baidu.com" "resolve")
at java.security.AccessControlContext.checkPermission(Unknown Source)
at java.security.AccessController.checkPermission(Unknown Source)
at java.lang.SecurityManager.checkPermission(Unknown Source)
at java.lang.SecurityManager.checkConnect(Unknown Source)
at java.net.InetAddress.getAllByName0(Unknown Source)
at java.net.InetAddress.getAllByName(Unknown Source)
at java.net.InetAddress.getAllByName(Unknown Source)
at org.apache.http.impl.conn.DefaultClientConnectionOperator.resolveHostname(DefaultClientConnectionOperator.java:242)
at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:130)
at org.apache.http.impl.conn.AbstractPoolEntry.open(AbstractPoolEntry.java:149)
at org.apache.http.impl.conn.AbstractPooledConnAdapter.open(AbstractPooledConnAdapter.java:121)
at org.apache.http.impl.client.DefaultRequestDirector.tryConnect(DefaultRequestDirector.java:573)
at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:425)
at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:820)
at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:754)
at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:732)
at com.taobao.cd.http.util.HttpUtil.createHtmlText(HttpUtil.java:38)
at com.taobao.cd.security.PolicyTest.network(PolicyTest.java:15)
at com.taobao.cd.security.PolicyTest.main(PolicyTest.java:45)
根据错误提示,知道是访问socket没有权限。那么修改下policy,指定权限:
grant {
permission java.net.SocketPermission "www.baidu.com:1-", "connect,resolve";
};
在指定权限文件下再运行,得到了正常的text形式的baidu首页的页面文档。权限策略成功。
代码参考github:https://github.com/changedi/CDLib/blob/master/src/main/java/com/taobao/cd/security/PolicyTest.java
2.5 附录
A
权限名 | 用途说明 |
accessClassInPackage.<name> | 允许代码访问指定包中的类 |
accessDeclaredMembers | 允许代码使用反射访问其他类中私有或保护的成员 |
createClassLoader | 允许代码实例化类加载器 |
createSecurityManager | 允许代码实例化安全管理器,它将允许程序化的实现对沙箱的控制 |
defineClassInPackage.<name> | 允许代码在指定包中定义类 |
exitVM | 允许代码关闭整个虚拟机 |
getClassLoader | 允许代码访问类加载器以获得某个特定的类 |
getProtectionDomain | 允许代码访问保护域对象以获得某个特定类 |
loadlibrary.<name> | 允许代码装载指定类库 |
modifyThread | 允许代码调整指定的线程参数 |
modifyThreadGroup | 允许代码调整指定的线程组参数 |
queuePrintJob | 允许代码初始化一个打印任务 |
readFileDescriptor | 允许代码读文件描述符(相应的文件是由其他保护域中的代码打开的) |
setContextClassLoader | 允许代码为某线程设置上下文类加载器 |
setFactory | 允许代码创建套接字工厂 |
setIO | 允许代码重定向System.in、System.out或System.err输入输出流 |
setSecurityManager | 允许代码设置安全管理器 |
stopThread | 允许代码调用线程类的stop()方法 |
writeFileDescriptor | 允许代码写文件描述符 |
B
权限名 | 用途说明 |
accessClipboard | 允许访问系统的全局剪贴板 |
accessEventQueue | 允许直接访问事件队列 |
createRobot | 允许代码创建AWT的Robot类 |
listenToAllAWTEvents | 允许代码直接监听事件分发 |
readDisplayPixels | 允许AWT Robot读显示屏上的像素 |
showWindowWithoutWarningBanner | 允许创建无标题栏的窗口 |
C
权限名 | 用途说明 |
specifyStreamHandler | 允许在URL类中安装新的流处理器 |
setDefaultAuthenticator | 可以安装鉴别类 |
requestPassworkAuthentication | 可以完成鉴别 |
D
权限名 | 用途说明 |
addIdentityCertificate | 为Identity增加一个证书 |
clearProviderProperties.<provider name> | 针对指定的提供者,删除所有属性 |
createAccessControlContext | 允许创建一个存取控制器的上下文环境 |
getDomainCombiner | 允许撤销保护域 |
getPolicy | 检索可以实现沙箱策略的类 |
getProperty.<prop name> | 读取指定的安全属性 |
getSignerPrivateKey | 由Signer对象获取私有密钥 |
insertProvider.<provider name> | 将指定的提供者添加到响应的安全提供者组中 |
loadProviderProperties.<provider name> | 装载指定的提供者的属性 |
printIdentity | 打印Identity类内容 |
putAllProviderProperties.<provider name> | 更新指定的提供者的属性 |
putProviderProperty.<provider name> | 为指定的提供者增加一个属性 |
removeIdentityCertificate | 取消Identity对象的证书 |
removeProvider.<provider name> | 将指定的提供者从相应的安全提供者组中删除 |
removeProviderProperty.<provider name> | 删除指定的安全提供者的某个属性 |
setIdentityInfo | 为某个Identity对象设置信息串 |
setIdentityPublicKey | 为某个Identity对象设置公钥 |
setPolicy | 设置可以实现沙箱策略的类 |
setProperty.<prop name> | 设置指定的安全属性 |
setSignerKeyPair | 在Signer对象中设置密钥对 |
setSystemScope | 设置系统所用的IdentityScope |
E
权限名 | 用途说明 |
enableSubstitution | 允许实现ObjectInputStream类的enableResolveObject()方法和ObjectOutputStream类的enableReplaceObject()方法 |
enableSubclassImplementation | 允许ObjectInputStream和ObjectOutputStream创建子类,子类可以覆盖readObject()和writeObject()方法 |
3、安全管理器、访问控制器和类装载器
3.1 安全管理器 SecurityManager
java.lang.SecurityManager
安全管理器在Java语言中的作用就是检查操作是否有权限执行。是Java沙箱的基础组件。我们一般所说的打开沙箱,也就是加-Djava.security.manager
选项。
其实日常的很多API都涉及到安全管理器,它的工作原理一般是:
- 请求Java API
- Java API使用安全管理器判断许可权限
- 通过则顺序执行,否则抛出一个Exception
比如在之前的“理解沙箱”这一章提到的,开启沙箱后,会限制文件访问,那这个代码时如何的呢?看下源码:
public FileInputStream(File file) throws FileNotFoundException {
String name = (file != null ? file.getPath() : null);
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
}
if (name == null) {
throw new NullPointerException();
}
if (file.isInvalid()) {
throw new FileNotFoundException("Invalid file path");
}
fd = new FileDescriptor();
fd.attach(this);
path = name;
open(name);
}
可以看到代码中先尝试获取安全管理器,如果开启沙箱,则安全管理器不是空,则需要检查读取权限checkRead(name)。而checkRead方法最内层的实现,其实利用了后面要说的访问控制器。
具体点,我们看下SecurityManager的主要方法列表:
checkAccept(String, int)
checkAccess(Thread)
checkAccess(ThreadGroup)
checkAwtEventQueueAccess()
checkConnect(String, int)
checkConnect(String, int, Object)
checkCreateClassLoader()
checkDelete(String)
checkExec(String)
checkExit(int)
checkLink(String)
checkListen(int)
checkMemberAccess(Class<?>, int)
checkMulticast(InetAddress)
checkMulticast(InetAddress, byte)
checkPackageAccess(String)
checkPackageDefinition(String)
checkPermission(Permission)
checkPermission(Permission, Object)
checkPrintJobAccess()
checkPropertiesAccess()
checkPropertyAccess(String)
checkRead(FileDescriptor)
checkRead(String)
checkRead(String, Object)
checkSecurityAccess(String)
checkSetFactory()
checkSystemClipboardAccess()
checkTopLevelWindow(Object)
checkWrite(FileDescriptor)
checkWrite(String)
都是check方法,分别囊括了文件的读写删除和执行、网络的连接和监听、线程的访问、以及其他包括打印机剪贴板等系统功能。而这些check代码也基本横叉到了所有的核心Java API上。
安全管理器可以自定义,作为核心API调用的部分,我们可以自己为自己的业务定制安全管理逻辑。举个例子如下:
public class SecurityManagerTest {
static class MySM extends SecurityManager {
public void checkExit(int status) {
throw new SecurityException("no exit");
}
}
public static void main(String[] args) {
MySM sm = new MySM();
System.out.println(System.getSecurityManager());
System.setSecurityManager(sm);//注释掉测一下
System.exit(0);
}
}
注释掉代码中的注释行,系统打印null,然后正常退出。当我们打开注释,并且自己扩展一个SecurityManager——MySM,它做的事情很简单,就是覆盖了checkExit方法,在系统退出时抛出一个“no exit”的异常。再执行,结果变成了
null
Exception in thread "main" java.lang.SecurityException: no exit
at com.taobao.cd.security.SecurityManagerTest$MySM.checkExit(SecurityManagerTest.java:7)
at java.lang.Runtime.exit(Runtime.java:107)
at java.lang.System.exit(System.java:971)
at com.taobao.cd.security.SecurityManagerTest.main(SecurityManagerTest.java:16)
显然,安全管理器生效了。
3.2 访问控制器 AccessController
java.security.AccessController
揭开沙箱面纱,第一步时安全管理器,那么第二步就是访问控制器了。因为沙箱的所有check方法实现,都是基于AccessController的。
3.2.1 组成
- 代码源CodeSource
CodeSource就是一个简单的类,用来声明从哪里加载类。
- 权限Permission
Permission类是AccessController处理的基本实体。Permission类本身是抽象的,它的一个实例代表一个具体的权限。权限有两个作用,一个是允许Java API完成对某些资源的访问。另一个是可以为自定义权限提供一个范本。权限包含了权限类型、权限名和一组权限操作。具体可以看看BasicPermission类的代码。典型的也可以参看FilePermission的实现。
- 策略Policy
策略是一组权限的总称,用于确定权限应该用于哪些代码源。话说回来,代码源标识了类的来源,权限声明了具体的限制。那么策略就是将二者联系起来,策略类Policy主要的方法就是getPermissions(CodeSource)和refresh()方法。Policy类在老版本中是abstract的,且这两个方法也是。在jdk1.8中已经不再有abstract方法。这两个方法也都有了默认实现。
在JVM中,任何情况下只能安装一个策略类的实例。安装策略类可以通过Policy.setPolicy()方法来进行,也可以通过java.security文件里的policy.provider=sun.security.provider.PolicyFile来进行。jdk1.6以后,Policy引入了PolicySpi,后续的扩展基于SPI进行。
- 保护域ProtectionDomain
保护域可以理解为代码源和相应权限的一个组合。表示指派给一个代码源的所有权限。看概念,感觉和策略很像,其实策略要比这个大一点,保护域是一个代码源的一组权限,而策略是所有的代码源对应的所有的权限的关系。
JVM中的每一个类都一定属于且仅属于一个保护域,这由ClassLoader在define class的时候决定。但不是每个ClassLoader都有相应的保护域,核心Java API的ClassLoader就没有指定保护域,可以理解为属于系统保护域。
3.2.2 AccessController
了解了组成,再回头看AccessController。这是一个无法实例化的类——仅仅可以使用其static方法。AccessController最重要的方法就是checkPermission()方法,作用是基于已经安装的Policy对象,能否得到某个权限。
回到理解沙箱那一篇文章里的例子,FileInputStream的构造方法就利用SecurityManager来checkRead。而SecurityManager的checkRead方法则
public void checkPermission(Permission perm) {
java.security.AccessController.checkPermission(perm);
}
这样来检查权限。
然而,AccessController的使用还是重度关联类加载器的。如果都是一个类加载器且都从一个保护域加载类,那么你构造的checkPermission的方法将正常返回。
当使用了其他类加载器或者使用了Java扩展包时,这种情况比较普遍。AccessController另一个比较实用的功能是doPrivilege(授权)。假设一个保护域A有读文件的权限,另一个保护域B没有。那么通过AccessController.doPrivileged方法,可以将该权限临时授予B保护域的类。而这种授权是单向的。也就是说,它可以为调用它的代码授权,但是不能为它调用的代码授权。
3.3 类装载器 ClassLoader
ClassLoader对安全模型有三方面的影响:第一,可以结合JVM定义名称空间,以保护Java语言本身安全特性的完整性。第二,在必要时调用SecurityManager保证代码在定义或者访问类时有适当的权限,第三,建立了权限与类对象之间的映射,这样AccessController就知道哪些类拥有哪些权限了。而这可以绕过建立自定义Policy类,通过自定义ClassLoader并在其中定义类权限而实现。
先来说说名称空间,其实就是包名,但是不同的是,不同的ClassLoader可以装在相同包名的类,而这时,其实对于每个ClassLoader,有一个自己的名字空间。为啥这么干?显然啊,就不说包冲突这事了,从安全角度看,你冒名顶替个co.sun.xx咋办?肯定得按照ClassLoader来分。从不同网站加载的Applet类,就是不同得ClassLoader来做。
3.3.1 老生常谈类加载器
类加载器是个层次结构,最基础的是系统类加载器,下面有很多子类比如URLClassLoader。加载一个类时,以委托的形式逐层询问,即类加载器加载类时会先尝试往上传递,由父加载类先尝试加载,若是父加载类无法加载,子加载类才会尝试加载。一旦为一个域的类定义类加载器,那么其他域的类加载器的整个祖先链路上不包含对应域,也就隔离了彼此的类加载。
3.3.2 类加载器加载类时要做哪些事
总的来说,需要完成以下工作:
- 1、询问安全管理器是否允许访问当前处理的类。如果不行,爬出一个安全异常。这一步可选,一般在loadClass()方法开始处实现。对应accessClassInPackage权限。
- 2、如果类加载器已经载入了此类,它将寻找以前定义的类对应,并返回该对象。这一步在loadClass()内部实现
- 3、否则,类加载器将询问其父亲,递归查看父类加载器是否知道如何载入此类。因此总会是系统类加载器最先加载,从而避免核心Java API中的类被其他自定义类冒充。这一步也在loadClass()里实现
2和3对应代码如下
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
4、询问安全管理器是否允许程序创建当前处理的类。如果不行,则抛出一个安全异常。这一步可选,如果实现,则需要在findClass()的开始处完成。这一步不是在操作开始时完成,而是在询问父类加载器之后进行,这一步对应为defineClassPackage权限。
- 5、向一个字节数组中读入类文件。读取文件以及创建字节数组的方式因类加载器不同而不同。在findClass()中完整
- 6、为该类创建合适的保护域。保护域可以来自默认安全模型(即从策略文件中得到),也可以由类加载器扩展。还有一种方法是可以创建一个代码源对象,并采用其保护域定义,这一步也在findClass()中完成。
- 7、在findClass()方法中,通过调用defineClass()方法。可以由字节码构造一个Class对象。如果使用的是第6步中的代码源,则需要调用getPermissions()方法查找于代码源相关的权限。defineClass()方法还保证了字节码必须铜鼓字节码校验器的检查。
- 8、最后还需要解析该类。即它所直接引用的类也应由当前类加载器找到。只有直接引用的才算,作为实例变量、方法参数或局部变量来使用的类不算。这一步在ladClass()中完成。对应上面代码中的resolveClass()。
3.3.3 以URLClassLoader举例
1对应的代码如下:
public final Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First check if we have permission to access the package. This
// should go away once we've added support for exported packages.
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
int i = name.lastIndexOf('.');
if (i != -1) {
sm.checkPackageAccess(name.substring(0, i));
}
}
return super.loadClass(name, resolve);
}
URLClassLoader的newInstance()方法会构造一个内部的工厂加载类。这个类的loadClass()方法做了checkPackageAccess的事情。
2,3 两步与超级父类ClassLoader相同。就是上面ClassLoader的loadClass()做的事情。
4,5,6,7 四个步骤涉及到findClass()方法,URLClassLoader覆盖了findClass()方法,但是最新版的jdk,其实将这几个步骤做的事情都在defineClass()里做掉了。里面的逻辑实现如下:
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
defineClass()的逻辑
/*
* Defines a Class using the class bytes obtained from the specified
* Resource. The resulting Class must be resolved before it can be
* used.
*/
private Class<?> defineClass(String name, Resource res) throws IOException {
long t0 = System.nanoTime();
int i = name.lastIndexOf('.');
URL url = res.getCodeSourceURL();
if (i != -1) {
String pkgname = name.substring(0, i);
// Check if package already loaded.
Manifest man = res.getManifest();
definePackageInternal(pkgname, man, url);
}
// Now read the class bytes and define the class
java.nio.ByteBuffer bb = res.getByteBuffer();
if (bb != null) {
// Use (direct) ByteBuffer:
CodeSigner[] signers = res.getCodeSigners();
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
return defineClass(name, bb, cs);
} else {
byte[] b = res.getBytes();
// must read certificates AFTER reading bytes.
CodeSigner[] signers = res.getCodeSigners();
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
return defineClass(name, b, 0, b.length, cs);
}
}
而这里面所有的defineClass都在ClassLoader这个超级父类里做了实现。
3.4 总结
通常的Java的安全性,都是从类加载器、安全管理器和访问控制器之间的关系考虑的。一般来说类加载器的作用更重要。
如果需要灵活的安全策略,往往要自定义类加载器。自定义类加载器允许在定义类时调整安全策略。这与实现一个新的Policy类相似。一般认为自定义类加载器会比修改一个Policy类要容易。
当你深入了解,你就会发现世界如此广袤,而你对世界的了解则是如此浅薄,请永远保持谦卑的态度。