四 访问控制机制和算法
4.1 java.security.ProtectionDomain
ProtectionDomain类囊括了域相关的一组特征。该域包括了一个类的集合,以一组当事人身份执行时这些类的实例会被赋予一组许可。一个保护域(ProductionDomain)由一个CodeSource、一个ClassLoader、一个Principals数组以及一个Permissions的集合组成。CodeSource囊括了本域内的所有类的代码库(java.net.URL),同时包括了一个公钥证书(java.security.cert.Certificate)的集合,对应着用于签名本域内所有代码的私钥。Principals代表了代码正在以什么身份运行。
在ProtectionDomain构造时传递进来的许可(permissions)代表了与该域绑定的一个静态的许可集合,该静态许可集合不考虑正在生效的安全规则(Policy)。ProtectionDomain随后在每次安全检查中咨询当前的安全规则从而获得该域被赋予的动态许可。
从不同的代码源而来的类,或者以不同的当事人身份执行的代码,属于不同的域(domain)。
今天,所有作为Java 2 SDK的一部分而集成的代码都被认为是系统代码,运行在唯一的系统域中。每一个applet或应用运行在在由规则(policy)决定的合适的应用程序域中。
确保任何非系统域中的对象不能自动发现其他非系统域中的对象,这是可行的。这种分区性可通过非常小心的类解析与加载,例如,对不同域使用不同的类加载器(classloader),来实现。然而,SecureClassLoader(或者其子类)可以,如果选择的话,加载来自不同域的类,这样就能允许这些类存在于同一个命名空间(就像被classloader分区一样)。
4.2 java.security.AccessController
AccessController被用来达到三个目的,每一个会在下面的段落里进行详细的描述:- 决定对一个重要系统资源的访问是会被允许还是拒绝,基于当前起正在作用的安全规则。
- 标记被执行了“privileged”的代码,如此一来就影响到随后的访问决定。
- 获得一个当前调用上下文的“快照”,如此的话从一个不同上下文做出的访问控制的决定就可以考虑到被保存的上下文。
ClassLoader loader = this.getClass().getClassLoader();
if(loader != null){
SecurityManager security = System.getSecurityManager();
if(security != null){
security.checkRead("path/file");
}
}
在新的体系下,无论一个调用类是否有关联的classloader,其安全检查都应该被调用。这可以很简单,例如:
FilePermission perm = new FilePermission("path/file", "read");
AccessController.checkPermission(perm);
AccessController的checkPermission方法检查当前的执行上下文并做出一个访问请求是否被允许的正确决定。如果被允许的话,该方法就安静的退出。否则,一个AccessControlException(java.lang.SecurityException的子类)被抛出。
注意到在一些遗留系统中,例如在一些浏览器中,SecurityManager是否注册意味着采用了导致不同行为的安全状态。为了向后兼容,SecurityManager的checkPermission方法可以如下使用:
SecurityManager security = System.getSecurityManager();
if(Security != null){
FilePermission perm = new FilePermission("path/file", "read");
security.checkPermission(perm);
}
我们当前并没有改变这方面对于SecurityManager的使用,但是会鼓励应用开发者在未来的开发中,如果内置的安全控制算法合适的话,使用Java 2 SDK引入的新的技术。SecurityManager的checkPermission方法的默认行为实际上是调用AccessController的checkPermission方法。一个不同的SecurityManager实现可以实现其自己的安全管理方法,很可能在决定是否允许一个访问请求时包含了更多的约束。
4.2.1 检查许可时的算法
设想访问控制检查发生在由多个调用者(caller)组成的调用链的计算线程中(可以想象为多个处于不同安全域的方法的调用链),如下图所示:如果调用链上的任何一个调用者(caller)没有这次请求所需要的许可,将会抛出AccessControlException,除非接下来的情况属实--一个其安全域拥有上述许可的调用者被标记为“privileged”(查看下一章)并且所有直接或者间接被该调用者调用的安全域都拥有上述的许可。很明显有两种实现策略:
在“积极评估”实现中,一个线程无论进入还是退出一个新的保护域,起作用的许可集合都会被动态的更新。
该实现策略的好处是,检查一个访问是否被允许是很简单的,并且在很多情况下是更快速的。不利之处在于,由于许可检查发生频率远比跨域调用低,因此相当大一部分跨域调用时许可的更新都是在做无用功。
在“懒惰评估”实现中,当一个许可检查被请求时,线程状态(由当前状态,包括当前线程的调用栈或其等价物,来表达)被检查,然后该访问请求是被拒绝还是被允许的决定会被做出来。
该方式的一个可能的负面影响是,在权限检查时性能表现不佳,虽然该情况在“积极评估”实现中也可能被引起(虽然一开始快,但在每一次跨域调用时都会扩展)。到目前为止我们的实现折中于可接受的性能表现,所以我们感觉“懒惰评估”是最划算的方案。
因此,当前的实现中对许可的检查算法使用的是“懒惰评估”。设想当前的线程跨越了多个方法调用,按照caller1到caller2到callerm的顺序。callerm调用了checkPermission方法。checkPermission用来决定一个访问请求是被允许还是拒绝的基本算法如下所示(在随后的章节有些许改良):
i = m;
while(i>0){
if(caller i's domain does not have the permission)
throw AccessControlException
else if(caller i is marked as privileged)
return;
i = i-1;
};
4.2.2 处理特权
AccessController的一个新的、静态的方法允许一个class对象的代码告知AccessController该代码的函数体是“特权的”,它自己负责对自己可达资源的访问请求,无论是什么代码调用导致的。也就是说,一个调用者(caller)可以被标记为“特权的”,当它调用doPrivileged方法。当做访问控制决定时,checkPermission方法在到达一个无上下文参数的“特权的”调用者时会直接停止检查(关于上下文参数的信息请看随后的章节)。如果“特权”调用者(caller)的保护域有该特定许可,checkPermission就不会再做进一步的检查,然后直接安静的退出,意味着访问请求被允许了。如果“特权的”调用者(caller)的保护域没有该特定许可,异常会像平常一样被抛出。
常见“特权”特性的使用如下所示:
如果你不需要从“特权”代码块中返回值,像如下代码:
somemethod() {
...normal code here...
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
// privileged code goes here, for example:
System.loadLibrary("awt");
return null; // nothing to return
}
});
...normal code here...
}
PrivilegeAction是一个只有一个方法的接口,该方法名为run,并且返回一个Object。上述的例子展示了一个实现了该接口的匿名内部类的创建;提供了一个run方法的具体实现。当调用doPrivilege时,一个PrivilegeAction实例被传递给它。doPrivilege方法在启用特权后调用该PrivilegeAction实现中的run方法,然后将该run方法中的返回值作为自己的返回值返回,在本例子中返回值被忽略。
(关于内部类的更多信息, 查看Java教程中的Nested Classes)
如果你需要返回一个结果,你可以像下面一样使用:
somemethod() {
...normal code here...
String user = (String) AccessController.doPrivileged(
new PrivilegedAction() {
public Object run() {
return System.getProperty("user.name");
}
}
);
...normal code here...
}
在你所写的run方法中的行为可能会抛出一个“checked”异常(在方法的throws语句中列出的异常),这样一来你需要使用PrivilegedExceptionAction接口来替代PrivilegedAction接口:
somemethod() throws FileNotFoundException {
...normal code here...
try {
FileInputStream fis = (FileInputStream)
AccessController.doPrivileged(
new PrivilegedExceptionAction() {
public Object run() throws FileNotFoundException {
return new FileInputStream("someFile");
}
}
);
} catch (PrivilegedActionException e) {
// e.getException() should be an instance of
// FileNotFoundException,
// as only "checked" exceptions will be "wrapped" in a
// <code>PrivilegedActionException</code>.
throw (FileNotFoundException) e.getException();
}
...normal code here...
}
一些关于特权代码的重要的点:首先,这个概念仅存在于单个线程中。一旦特权代码完成,该特权许可立刻会被清除或撤销。
第二,在该例子中,run方法中的函数体被标记为“特权的”。然而,如果它调用了低可信度并被授予更少权限的代码,该代码不会获得任何特权权限;只有在特权代码拥有相应权限并且该特权代码随后调用的直到checkPermission的调用者(caller)这整个调用链上的所有调用者(caller)都拥有相应权限时,才会赋予许可权。
查看如下网址获得将代码标记为“特权的”更多的信息 https://docs.oracle.com/javase/8/docs/technotes/guides/security/doprivileged.html 。
4.3 Access Control Context的继承
当一个线程创建了一个新线程,一个新的栈也被创建。如果在创建新线程时当前的安全上下文不被保存,那么当在新线程中调用AccessController.checkPermission时,安全决定将仅仅根据新线程的安全上下文做出,不会考虑父线程的安全上下文。本质上说这个干净栈的问题并不是一个安全问题,但是它会使得编写安全代码,尤其是系统代码,更容易产生难易察觉的错误。例如,一个非专家的开发者可能会想当然的假定一个子线程(例如:一个没有涉及到不信任代码的)会从父线程(例如:一个涉及到不安全代码的)继承相同的安全上下文。而这将会导致不希望的安全漏洞,例如当从新创建的线程中访问受控的资源时(然后将该资源传递给不受信任的代码),如果父线程安全上下文没有被保存的话。
因此,当一个新线程被创建,我们实际上会确保(通过线程创建以及其他代码)在该子线程创建时自动的继承了父线程的安全上下文,以此来保证子线程中随后的checkPermission会考虑到继承的父线程安全上下文。
换句话说,逻辑上的线程安全上下文包括了父线程安全上下文(以AccessControlContext的形式,将在下一章描述)和当前调用栈的安全上下文,许可检查的算法扩展如下。(回想有m个caller直到调用checkPermission的caller,查看下一章来了解AccessControlContext的checkPermission方法。)
i = m;
while (i > 0) {
if (caller i's domain does not have the permission)
throw AccessControlException
else if (caller i is marked as privileged)
return;
i = i - 1;
};
// Next, check the context inherited when
// the thread was created. Whenever a new thread is created, the
// AccessControlContext at that time is
// stored and associated with the new thread, as the "inherited"
// context.
inheritedContext.checkPermission(permission);
注意这种继承是可传递的,以至于一个祖孙线程会继承父线程和祖父线程的安全上下文。同样注意继承的上下文快照是在线程创建的时候取得,而不是线程第一次run的时候取得。并且没有开放的API可以修改这个继承特性。
4.4 java.security.AccessControlContext
回想在当前执行线程的安全上下文(包含了继承的安全上下文)中AccessController的checkPermission方法执行安全检查的情景。当这样一个安全检查只能在另外一个安全上下文中执行时候,困难就出现了。就是说,有些时候一个本应该在某特定安全上下文中执行的安全检查,实际上需要在另外一个不同的安全上下文中完成。例如,当一个线程向另外一个线程发送了一个事件,第二个线程在处理这个请求事件的时候没有正确的安全上下文来完成访问控制,如果这个服务需要请求访问受控资源的话。为了解决该问题,我们为AccessController提供了getContext方法并提供了AccessControlContext类。getContext方法获取一个当前调用的安全上下文的“快照”,将其放在一个AccessControlContext对象中,并返回该对象。一个调用的样例如下:
AccessControlContext acc = AccessController.getContext();
该上下文快照抓取了相关的信息,所以一个访问控制决定可以在另外一个上下文中通过检查该上下文信息做出。例如,一个线程可以发送事件给另外一个线程,同时也提供当前的上下文快照信息。AccessControlContext本身有一个checkPermission方法,该方法基于它自己包含的安全上下文来做出访问控制决定,而不是当前执行线程的安全上下文。这样一来,如有必要,第二个线程就可以通过调用下面的代码来执行一个合适的安全检查:
acc.checkPermission(permission);
上面的方法调用等价于在第一个线程中执行安全检查,即使它是在第二个线程中执行的。
有时候一些安全控制上下文许可必须被检查,但是事先又不清楚是哪些许可。在这些情况下,你可以使用带上下文的doPrivilege方法:
somemethod() {
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
// Code goes here. Any permission checks from
// this point forward require both the current
// context and the snapshot's context to have
// the desired permission.
}
}, acc);
...normal code here...
现在AccessController的checkPermission方法所使用的完整算法可以被给出了。假设当前线程跨越了m个调用者,按照caller1到caller2到callerm的顺序。然后callerm调用了checkPermission方法。checkPermission用来决定访问请求是被允许还是拒绝的算法如下:
i = m;
while (i > 0) {
if (caller i's domain does not have the permission)
throw AccessControlException
else if (caller i is marked as privileged) {
if (a context was specified in the call to doPrivileged)
context.checkPermission(permission);
return;
}
i = i - 1;
};
// Next, check the context inherited when
// the thread was created. Whenever a new thread is created, the
// AccessControlContext at that time is
// stored and associated with the new thread, as the "inherited"
// context.
inheritedContext.checkPermission(permission);
===========================================================================
本技术文档的翻译工作由不动明王1984独自完成,特此声明。
翻译辛苦,珍惜劳动,引用时请注明出处!
===========================================================================
上一篇:Java Security Architecture--Java安全体系技术文档翻译(三)
下一篇:Java Security Architecture--Java安全体系技术文档翻译(五)