级别: 中级 李 三红 (lisanh@cn.ibm.com), 高级软件工程师, IBM
2009 年 10 月 30 日 Jobs API 被广泛的应用到 Eclipse 平台中,Job 是 Eclipse 运行时重要的组成部分,它可以理解成被平台调用异步运行的代码块,多个 Jobs 可以并发执行。对于基于 Eclipse 平台开发并发应用的开发者来说,Eclipse 提供的 Job 框架很好地替代了 Java 原生的 Thread, 其内在使用了线程池实现,具有更好的伸缩性,更好的性能等。不过问题是,在运行时安全方面,现有的 Eclipse Job 框架并不能保证线程间的安全协作,不能在 Job 运行时检查 Job 创建者的权限。本文所要讨论的就是 Eclipse Job 的运行时安全问题,并提供相应的解决方案。
再开始我们正式的讨论之前,我们简单回顾一下 Java2 所提供的运行时安全模型。 Java2 运行时安全模型可以概括理解为基于策略 (Policy) 与堆栈授权的运行时安全模型。
我们通过一个简单的例子来看一下,如图 1 所示:
图 1. Java2 运行时安全检查
假设当前的运行时堆栈是从 a.class 到 e.class 。在运行时堆栈上的每一帧 (Stack Frame) 都会被 Java 划归为某个保护域 (ProtectionDomain),保护域是 Java 根据 Policy 文件配置构建出来的。
我们假设当前帧 e.class 的某个方法需要打开本地磁盘的某个文件,由于执行打开文件操作属于存取系统资源,这将触发 Java 的安全管理器 (SecurityManager) 执行权限检查,它会对堆栈上的每个 Stack Frame 做权限检查,也就是从当前帧 e.class 开始,到 a.class 止。,当且仅当每个 Stack Frame 被赋予的权限集都暗含 (Imply) 了打开该文件的权限时,该操作才被允许执行,否则 java.security.AccessControlException 例外将被抛出,该操作执行失败。
另外,在多线程的环境下,例如当父线程创建了子线程,子线程的执行被看作是父线程执行的继续,所以 Java 安全管理器在权限检查时,所检查的运行时堆栈,既包括当前子线程的,也包括从父线程那里继承过来的运行时堆栈。
Eclipse Jobs 框架
在这节,我们讨论一下和本文相关的 Eclipse Jobs 的内容。使用 Eclipse Job 样例代码如清单 1 所示:
清单 1. Eclipse Job API
Job sampleJob = new Job("the sample job") {
@Override
protected IStatus run(IProgressMonitor monitor) {
//business logic
return Status.OK_STATUS;
}
};
sampleJob.schedule(); |
图 2 是 Eclipse Job 简略的调度序列图。在这里,本文并不打算过多讨论 Eclipse Jobs 框架本身内容,更多关于 Eclipse Job 的细节性内容,请读者参考 developerWorks 上相关文章。
图 2. Eclipse Job 调度序列图
通过调度序列图,我们可以总结出以下几点:
- Eclipse Job 的执行,实际上牵涉到两个线程,即:调用 Job schedule 方法的用户线程和 Eclipse 内在的 Worker 线程。
- Eclipse Job 是被异步执行的,即:用户线程调用 Job 的 schedule 方法,该 Job 实际上是被插入了 Job 队列,在后来的某个时间点上,被 Worker 线程从队列中取出,异步执行。
- Eclipse Job 是由线程池里的空闲线程 (Worker) 负责执行的,Worker 本身继承于 Java 的 Thread 线程。特别注意的是,在 Job schedule 方法被调用的时候,如果线程池里没有任何的线程,那么新的 Worker 线程会被创建,放入线程池里,并且调用该线程的 start 方法。
以上几点是本文讨论问题的核心所在。
问题
从本质上讲,Eclipse 的 Jobs API 是基于线程池 (WokerPool) 和消息队列 (JobQueue) 范例的线程协作机制。在这样的模型下,如果把运行时的安全因素考虑进来,问题会变得复杂。基于现有的 Jobs API,我们没法保证该模型下的协作安全,图 3 很能说明这个问题:
图 3. Eclipse Job 运行机制
图 3 描述的 Eclipse Job 运行机制的本质。用户线程与 Eclipse Worker 线程通过消息队列进行异步协作。这种异步的本质,造成了线程执行堆栈的不连续性。这样的结果是:Java 只要求 Eclipse Worker 线程具有执行 Job 所定义的逻辑的权限,而对于 Job 的创建者,没有任何的权限约束。从安全的角度考虑,这显然是违背原则的:在一般情况下,Eclipse Worker 线程作为 Eclipse 平台的一部分,具有存取任何资源的权限,从这个角度来说,要求 Eclipse Worker 线程具有执行 Job 所定义的逻辑的权限 , 没有任何意义。事实上,它是代表用户线程执行 Job 所定义的逻辑,要求 Job 的创建者具有执行 Job 所定义的逻辑的权限,才具有实质性的意义。
我们所期望的结果是,在 Job 被调度运行时,能够沿着如图 4 所示的虚线箭头,检查 Job 创建者的执行堆栈。
图 4. Eclipse Job 运行时堆栈
解决思路
Java 提供了线程间安全协作的解决方案,即:使用 AccessController.doPrivileged 方法,把你想要检查的执行堆栈快照传入即可,如清单 2 所示:
清单 2. Java 线程安全协作
AccessController.doPrivileged(
new PrivilegedAction<Object>() {
public IStatus run() {
// 定义你自己的逻辑。
……
}
},
accesscontrolcontext
// 将用户线程当时的执行环境快照传入,进行权限检查。
); |
接下来,我们讨论,如何将之应用于 Eclipse Job 框架。如清单 3 所示:
清单 3. 安全的 Job 类
abstract public class SecureJob extends Job{
AccessControlContext _accesscontrolcontext;
public SecureJob(String name) {
super(name);
//Job 将在用户线程里创建。
// 这时,我们把用户线程的执行环境通过 AccessController.getContext() 得到,
// 并及时保存下来。
_accesscontrolcontext = AccessController.getContext();
}
@Override
protected IStatus run(IProgressMonitor monitor) {
final IProgressMonitor fm = monitor;
IStatus status = AccessController.doPrivileged(
new PrivilegedAction<IStatus>(){
public IStatus run() {
// 调用用户定义的逻辑
return secureRun(fm);
}
},
_accesscontrolcontext
// 将用户线程当时的执行环境传入,进行权限检查。
);
return status;
}
// 子类通过重载定义自己的逻辑。
abstract protected IStatus secureRun(IProgressMonitor monitor);
public void secureSchedule() {
AccessController.doPrivileged
(
new PrivilegedAction<Object>()
{
public IStatus run()
{
SecureJob.super.schedule();
return null;
}
}
);
}
} |
可以看到,SecureJob 继承于 Eclipse Job 类,它定义了用于子类重载的方法 secureRun 。任何通过继承 SecureJob,实现了 secureRun 方法的 Job 类都是安全的,它的执行都会要求 Job 创建者具有相应的运行时权限。
SecureJob 实现的运行时权限检查路径如图 5 所示:
图 5. 基于 SecureJob 的权限检查路径
图 5 中的箭头指示了 Java 安全管理器权限检查的路径,从当前的帧 (Frame) 开始,沿着 Eclipse worker 线程的运行时堆栈检查,直到碰到了 AccessController.doPrivileged 帧。由于我们在调用 doPrivileged 方法时,传入了 _accesscontrolcontext,也就是用户线程构造 SecureJob 对象时的执行环境,所以 Java 的安全管理器会跳转到该执行环境,沿着用户线程构造 SecureJob 时的执行堆栈逐一检查。
特别要注意的是,SecureJob 实现了自己的 secureSchedule 方法。在这里为什么要实现自己的 schedule 方法呢?
首先,Job 的 schedule 方法被定义为 final 而不能被重写 (override), 所以 SecureJob 不得定义了新的 secureSchedule 方法。 secureSchedule 实际上是在 doPrivileged 中调用了父类的 schedule 方法。图 6 可以给我们解释为什么这里我们必须定义自己的 secureSchedule 方法,并且使用 doPrivileged 。
图 6. SecureJob 的 secureSchedule 方法用处
我们假设,用户线程 1(User thread 1) 调用 Job schedule 方法的时候,当前线程池是空的,于是新的 Worker 线程被创建出来,这种情况下,Worker 线程是用户线程 1 的子线程,我么知道,Java2 运行时安全模型本身对多线程是有支持的,即:子线程的执行被看作是父线程执行的继续,运行时安全检查的堆栈,既包括当前子线程的,也包括父线程的。当用户线程 2 调用 Job 的 schedule 方法,如果执行用户线程 2 创建的 Job 任务的线程与用户线程 1 使用了同样 Worker 线程 ( 如图 5 所示的情况 ),那么权限检查就会出现非常怪异的情况:很明显,当前 Worker 线程执行的是用户线程 2 创建的任务,但是,运行时的权限检查会检查到用户线程 1 的执行堆栈去,这是因为 Worker 线程是用户线程 1 的子线程。这当然是我们不希望看到的结果,这样也是不合逻辑的。所以我们不得不重写 Job 的 schedule 方法,通过使用 doPrivileged,使得用户线程 1 调用 Job schedule 方法时的执行堆栈并不会在检查 Worker 线程时而被包括进来。
这里,问题的本质是,放在线程池里的线程可能会被任何用户线程使用,所以我们不能把从父线程继承过来的执行环境快照 ( 也就是 AccessControlContext 对象,通过 AccessController.getContext() 调用获得 ) 强加给共用的线程池线程。
示例
本节给出使用 SecureJob 的例子,供读者参考。
清单 4 给出一个简单的 Log 服务实现,把 Message 写到某个磁盘文件
清单 4. LogService
public class LogService {
……
private void log(Message message) throws InvalidAccessException {
final String destination = message.getDestination();
final String info = message.getInfo();
FileWriter filewriter = null;
try {
filewriter = new FileWriter(destination, true);
filewriter.write(info);
filewriter.close();
} catch(AccessControlException ace) {
throw new InvalidAccessException(ace);
} catch (IOException ioexception) {
ioexception.printStackTrace();
}
}
……
}
出于性能优化方面的考虑,为了使使用 Log 服务的线程不需要同步等待 Message 被写入到磁盘, |
清单 5 和清单 6 各自定义了异步的输出 Log Message 的方法。清单 5 使用了 Eclipse 本身提供的 Job 类,而清单 6 使用了我们上一节定义的SecureJob 类。
清单 5. 使用 Eclipse Job 异步输出 Message
public class LogService {
……
public void asyncLog(final Message message) {
final Job myJob = new Job("log message") {
@Override
protected IStatus run(IProgressMonitor monitor) {
try {
LogService.instance.log(message);
} catch (InvalidAccessException e) {
e.getACE().printStackTrace();
}
return Status.OK_STATUS;
}
};
AccessController.doPrivileged
(
new PrivilegedAction<Object>()
{
public IStatus run()
{
myJob.schedule();
return null;
}
}
);
}
……
} |
清单 6. 使用 SecureJob 异步输出 Message
public class LogService {
……
public void secureAsyncLog(final Message message) {
final Job myJob = new SecureJob("log message") {
@Override
protected IStatus secureRun(IProgressMonitor monitor){
try {
LogService.instance.log(message);
} catch (InvalidAccessException e) {
e.getACE().printStackTrace();
}
return Status.OK_STATUS;
}
};
((SecureJob)myJob).secureSchedule();
}
……
} |
清单 7 给出了测试 Log Service 的代码:
清单 7. 使用 LogService
private void logSomething() {
Message message = new Message("c:/paper/secure_job/out.tmp",
"Hi, the bundle \"sample.security.test\"
has been started."+'\n');
LogService.instance.asyncLog(message);
LogService.instance.secureAsyncLog(message);
} |
logSomething 方法在 sample.security.test Plugin 启动的时候被调用,请求 LogService 往磁盘文件 c:/paper/secure_job/out.tmp 写入该 Plugin 被启动的日志。在这里我们分别调用了 ayncLog 与 secureAsyncLog 方法。
在本文所给的这个例子中,有三个 Plugin:,即:sample.security.job,sample.security.test.logservice 以及 sample.security.test 。我们把它们放在 OSGi Framework 中运行,并通过清单 8 指示 OSGi 运行时使用的安全管理器。
清单 8. 安装 OSGi 运行时安全管理器
-Declipse.security=org.eclipse.osgi.framework.internal.core.FrameworkSecurityManager |
运行的结果正如我们所期望的,LogService 的 asyncLog 方法由于使用的是 Eclipse Job,并没有强制的运行时安全检查,所以该方法会被成功执行。而 secureAsyncLog 则执行失败,InvalidAccessException 被抛出,这是由于对于调用者 sample.security.test Plugin 而言,它并没有文件 c:\paper\secure_job\out.tmp 的写权限,如果期望 secureAsyncLog 能够被正确执行,必须显式地赋予 sample.security.test Plugin 写 c:\paper\secure_job\out.tmp 文件的权限,这可以通过设置 Plugin 的 OSGI-INF/permissions.perm 文件完成,清单 9 是我们为 sample.security.test Plugin 指定的权限集。
清单 9. sample.security.test Plugin 权限设定
(org.osgi.framework.BundlePermission "org.eclipse.core.runtime" "require")
(org.osgi.framework.PackagePermission "sample.security.test.logservice" "import")
(java.lang.RuntimePermission "setContextClassLoader")
(java.io.FilePermission "c:/paper/secure_job/out.tmp" "write") |
如清单 9 的最后一项所示,我们指定了 sample.security.test Plugin 有写 c:\paper\secure_job\out.tmp 文件的权限。使用清单 9 给出的权限设定,secureAsyncLog 就能够成功被执行。
关于完整的源代码,读者可以在本文后面的资源列表中下载。
特别声明:本文所提供的代码都是示例性的,目的只是为了帮助读者更好地理解 Eclipse Job 框架线程协作方面的安全漏洞,并提供读者相应的解决思路,但并不保证能够直接用来解决实际开发中所面临的问题。
小结
对于基于 Eclipse 平台开发的 Java 开发者来说,Eclipse Job 以其简洁、易用、更好的性能等优点,成为了 Java Thread 的很好替代方式。目前大量的基于 Eclipse 的 RCP 应用使用 Job 完成 UI 后端的逻辑。正如本文所探讨的那样,目前 Eclipse Job API 并没有对 Java 运行时安全提供显式支持。本文通过继承 Job 的方式,提供了读者这方面的一个参考实现。这种通过继承 Job 实现自己安全类的方式,可以很容易用在新应用的开发中。不过问题是,对于现存的,遗留的大量使用 Eclipse Job 的代码,怎么样平滑地重构现有应用,以期实现运行时安全,仍然是个大的问题。
参考资料
关于作者  | |  | 李三红,任职于 IBM CDL,负责 Lotus Notes 产品研发。在这之前,作者一直从事网络应用开发相关工作。作者感兴趣的技术领域包括:分布式对象计算,网络应用,OSGi,协作计算,Java 安全等方面。 |
对本文的评价
|