在 Java 之前的版本运行时的安全模型使用非常严格受限的沙箱模型(Sandbox)读者应该熟悉Java 不受信的 Applet 代码就是基于这个严格受限的沙箱模型来提供运行时的安全检查沙箱模型的本质是任何本地运行的代码都是受信的有完全的权限来存取关键的系统资源而对于 Applet则属于不受信的代码只能访问沙箱范围内有限的资源当然您可以通过数字签名的方式配置您的 Applet 为受信的代码具有同本地代码一样的权限
从 Java 开始Java 提供了基于策略(Policy)与堆栈授权的运行时安全模型它是一个更加细粒度的存取控制易于配置与扩展其总体的架构如图 所示
图 Java 安全模型
简单来讲当类由类装载器(Class Loader)载入到 JVM 运行这些运行时的类会根据 Java Policy 文件的配置被赋予不同的权限当这些类要访问某些系统资源(例如打开 Socket读写文件等)或者执行某些敏感的操作(例如存取密码)时Java 的安全管理器(avalangSecuirtyManager)的检查权限方法将被调用检查这些类是否具有必要的权限来执行该操作
在继续深入讨论之前我们先来澄清下面的几个概念
策略即系统安全策略由用户或者管理员配置用来配置执行代码的权限运行时的 javasecurityPolicy 对象用来代表该策略文件
权限Java 定义了层次结构的权限对象所有权限对象的根类是 javasecurityPermission权限的定义涉及两个核心属性目标(Target)与动作 (Action)例如对于文件相关的权限定义其目标就是文件或者目录其动作包括读写删除等
保护域保护域可以理解为具有共同的权限集的类的集合
在 Java 里权限实际上是被赋予保护域的而不是直接赋给类权限保护域和类之间的映射关系如图
图 类保护域权限的映射关系
如图 所示当前运行时堆栈是从 aclass 到 eclass在运行时堆栈上的每一帧(Stack Frame)都会被 Java 划归为某个保护域(保护域是 Java 根据 Policy 文件配置构建出来的)Java 的安全管理器在执行权限检查时会对堆栈上的每个 Stack Frame 做权限检查当且仅当每个 Stack Frame 被赋予的权限集都暗含(Imply)了所要求的权限时该操作才被允许执行否则 javasecurityAccessControlException 异常将被抛出该操作执行失败
有关 Java 安全模型有几点需要特别说明
该模型是基于堆栈授权的这在多线程的环境下同样适用例如当父线程创建了子线程子线程的执行被看作是父线程执行的继续所以 Java 的安全管理器在权限检查时所检查的运行时堆栈既包括当前子线程的也包括从父线程那里继承过来的运行时堆栈这意味着用户不可能通过线程的创建来获得额外的权限
Java 的开发者可以使用 AccessControllerdoPrivileged 来优化权限检查带来的额外性能开销如图 所示Java 的权限检查将从堆栈的顶部开始逐一向下直到碰到 doPrivileged 的方法调用或者到达堆栈底部为止使用 doPrivileged 可以避免不必要的栈遍历(Stack Traverse)提高程序的性能
在该模型中有一个特殊的保护域系统域(System Domain)所有被 null类装载器所装载的类都被称为系统代码其自动拥有所有的权限而且所有的重要的受保护的外部资源如文件系统网络屏幕键盘等只能通过系统代码获得
图 doPrivileged Stack Frame
接下来本文会给出一个简单的示例然后我们根据这个示例进一步深入来创建一个线程间安全协作的应用
示例
我们的示例很简单客户端调用 LogService 提供的 API把 Message 写入到磁盘文件
清单 客户端程序
packagesamplepermtestclient
……
public class Client {
……
public static void main(String[] args){
//构造消息日志使用LogService将其写入c\\paper\\client\\outtmp文件
Messagemessage=newMessage(c\\paper\\client\\outtmpHithisiscalledfromclient+\n)LogServiceinstancelog(message)
//构造消息日志使用LogService将其写入c\\paper\\server\\outtmp文件
message=newMessage(c\\paper\\server\\outtmpHithisiscalledfromclient+\n)LogServiceinstancelog(message)
}
清单 LogService
packagesamplepermtestserver
……
public class LogService {
……
public void log(Message message){
finalStringdestination=messagegetDestination()finalStringinfo=messagegetInfo()
FileWriter filewriter = null
try
{
filewriter=newFileWriter(destinationtrue)filewriterwrite(info)filewriterclose()
}
catch(IOException ioexception)
{
ioexceptionprintStackTrace()
}
如清单 所示这就是一个普通的 Java 应用程序我们把这个程序放在 Java 的安全模型中执行Client 类放在 clientjar JAR 包里而 LogService 类放在 serverjar JAR 包里
首先我们使用 keytool 工具来生成我们需要的 keystore 文件以及需要的数字证书如清单 所示
清单 生成 keystore 文件及其数字证书
>keytoolgenkeyaliasclientkeyalgRSAkeystoreC:\paper\keystore
>keytoolgenkeyaliasserverkeyalgRSAkeystoreC:\paper\keystore
在清单 中我们生成了 C\paper\keystore 文件使用 RSA 算法生成了别名为 client 与 server 的两个数字证书(注 为方便起见keystore 与 clientserver 证书的密钥都是 )
我们使用如清单 所示的命令来签名 clientjar 与 serverjar
清单 签名 JAR 文件
>jarsignerexekeystoreC:\paper\keystore
storepassc\paper\clientjarclient
>jarsignerexekeystoreC:\paper\keystore
storepassc\paper\serverjarserver
在清单 中我们使用了别名为 client 的数字证书来签名 clientjar 文件使用别名为 server 的数字证书来签名 serverjar 文件
使用图形化的工具 policytoolexe 创建清单 所示的 Policy 文件
清单 Policy 文件
/* AUTOMATICALLY GENERATED ON Thu May CST */
/* DO NOT EDIT */
keystorefile:////C/paper/keystore
grant signedBy client {
permissionjavaioFilePermissionc\\paper\\client\\*readwrite
}
grant signedBy server {
permissionjavasecurityAllPermission
}
Policy 文件指出所有被client签名的代码具有读写 c\\paper\\client\\目录下所有文件的权限而所有被server签名的代码具有所有的权限Java 将根据该策略文件按照签名者创建相应的保护域
一切就绪我们运行代码如清单 所示
清单 运行程序
>javaDjavasecuritymanager
Djavasecuritypolicy=mypolicyclasspathclientjarserverjar samplepermtestclientClient
有两个运行时选项特别重要Djavasecuritymanager 告诉 JVM 装载 Java 的安全管理器进行运行时的安全检查而 Djavasecuritypolicy 用来指定我们使用的策略文件
运行的结果如清单 所示
清单 运行结果
ExceptioninthreadmainjavasecurityAccessControlExceptionaccessdenied(javaioFilePermissionc\paper\server\outtmpwrite)
atjavasecurityAccessControlContextcheckPermission(UnknownSource)
atjavasecurityAccessControllercheckPermission(UnknownSource)
atjavalangSecurityManagercheckPermission(UnknownSource)
atjavalangSecurityManagercheckWrite(UnknownSource)
atjavaioFileOutputStream(UnknownSource)
atjavaioFileWriter(UnknownSource)
atsamplepermtestserverLogServicelog(LogServicejava)
atsamplepermtestclientClientmain(Clientjava)
客户端运行后第一条消息成功写入 c\\paper\\client\\outtmp 文件而第二条消息由于没有 c\paper\server\outtmp 文件的写权限而被拒绝执行
线程间的安全协作
前一节本文给出的示例如果放在线程间异步协作的环境里情况会变得复杂如图 所示
图 线程的异步协作
如图 在这样的情景下客户端线程的运行时堆栈完全独立于服务器端的线程它们之间仅仅通过共享的数据结构消息队列进行异步协作例如当客户端线程放入 Message X而后服务器端的线程拿到 Message X 进行处理我们仍然假设 Message X 是希望服务器端线程将消息写入 c\paper\server\outtmp 文件在这个时候服务程序怎样才能确保客户端具有写入 c\paper\server\outtmp 文件的权限?
Java 提供了基于线程协作场景的解决方案如清单 所示
清单 线程协作版本的 LogService
packagesamplepermtestthreadserver
……
public class LogService implements Runnable
{
……
public synchronized void log(Message message)
{
//该方法将在客户端线程环境中执行
//在消息放入队列的时候我们把客户端线程的执行环境通过//AccessControllergetContext()得到//并及时保存下来
messagem_accesscontrolcontext=AccessControllergetContext()_messageListadd(message)
notifyAll()
}
……
//从队列中取出消息并逐一处理
public void run()
{
while(true)
{
Message message = null
try
{
message = retrieveMessage()
}
catch(InterruptedException interruptedexception)
{
break
}
finalStringdestination=messagegetDestination()finalStringstringMessage=messagegetInfo()AccessControllerdoPrivileged
(
new PrivilegedAction()
{
public Object run()
{
FileWriter filewriter = null
try
{
filewriter=newFileWriter(destinationtrue)filewriterwrite(stringMessage)filewriterclose()
}
catch(IOException ioexception)
{
ioexceptionprintStackTrace()
}
return null
}
}messagem_accesscontrolcontext //将客户端线程当时的执行环境传入进行权限检查
)
}
消息类的 m_accesscontrolcontext 成员变量是一个 AccessControlContext 对象它封装的当前线程的执行环境快照我们可以通过调用 AccessController 的 getContext 方法获得安全的线程协作工作原理如图 所示
图 线程异步协作权限检查路径
图 中的箭头指示了 Java 的安全管理器权限检查的路径从当前的帧 (Frame) 开始沿着服务器端线程的运行时堆栈检查直到碰到了 AccessControllerdoPrivileged 帧由于我们在调用 doPrivileged 方法时传入了 m_accesscontrolcontext也就是客户端线线程在往消息队列里插入消息时的执行环境所以 Java 的安全管理器会跳转到该执行环境沿着客户端插入消息时的执行堆栈逐一检查
在本节线程版本的 Log 服务实现中Client 类在 samplepermtestthreadclient 包里该包被导出为 thread_clientjar JAR 包而 LogService 在 samplepermtestthreadserver 包里该包被导出为 thread_serverjar JAR 包而有关这部分的包签名与上节类似使用了与上节相同的数字证书
关于完整的源代码读者可以在本文后面的资源列表中下载
小结
本文通过示例详尽描述了 Java 运行时的安全模型特性以及基于该模型如何构建安全的线程协作应用值得一提的是当您的 Java 应用使用的 Java 所提供的运行时安全模型程序性能的降低是必然的因为我们已经看到Java 的安全模型是基于堆栈授权的这意味着每一次 Java 安全管理器检查权限方法的执行都会遍历当前运行时行堆栈的所有帧以确定是否满足权限要求所以您的设计一定要在安全与性能之间取捨当然当您在应用了 Java 的安全模型后您仍然有机会进行性能的优化比如使用 doPrivileged 方法去掉不必要的堆栈遍历更进一步您可以根据自己应用的特点通过继承 javalang SecurityManager 类来开发适合自己应用的安全管理器