Java 授权内幕:
以代码为中心的 Java 2 平台安全体系结构和以用户为中心的 Java 认证和授权服务。
在信息安全性领域,授权是世界的的中心,因为它是控制个体(即人、进程和计算机)对系统资源的访问权限的过程。直到最近,在 Java 安全体系结构中相关的问题都是
“这段运行中的代码的访问权限是什么?”。在 JAAS 中,相关问题变成了“
运行这段代码的认证用户的访问权限是什么?”
在本文中,将同时介绍
老的以代码为中心的
Java
授权体系结构 和
新的以用户为中心的体系结构。我将首先对 Java 2 平台安全体系结构作一概述,重点放在
这个体系结构如何利用两个基本概念
--
安全策略和保护域
--
来定义、组织和聚集静态和
/
或动态访问权限 。 然后详细分析 Java 2 平台安全体系结构的运行时访问检查功能的底层机制,包括堆栈检查和确定是否授予权限的遍历(traversal)机制。在了解了以代码为中心的授权模型是 如何工作的后,我将转向 Java 授权和认证服务(JAAS)的以用户为中心的授权模型。在这里,我将重点放到基于 subject 的访问控制这一概念上,并展示在 JAAS 中,它是如何在原来 Java 2 平台安全体系结构的堆栈检查机制之上实现的。
1
以代码为中心的授权
Java 平台传统上是用来运行移动代码的,如 applet。为了保护系统资源不被下载到用户浏览器中的任意代码片段所破坏,applets 被限制到一个沙箱中,它们在这里以有限的一组权限运行 。另一方面,本地应用程序通常受到信任可以访问所有系统资源。
JDK 1.x 模型和 Java 2 平台 SDK 版本 1.2 的新安全结构之间的最大区别是引入了新的、可配置的安全策略,这样就可以实现细化的和可管理的访问控制。所有代码(不管是本地还是下载的,不管是签名或者 没有签名的)都可以受到定义良好的安全策略的约束,它为不同的代码授予(可能是重叠的)权限。同时,
随着在
JVM
中引入了多进程能力(请参阅
参考资料),出现了对基于用户的访问控制的要求 。
Java 2 平台安全体系结构背后的基本原理可以总结如下:
一个系统级的
安全策略
(
按照应用程序的需要)定义了按
保护域
组织的执行代码的
访问权限
。安全策略用于访问控制检查,这是由
JVM
在运行时执行的。 在本次导游中,我将逐一详细阐述这些概念。
1.1
访问权限作为类型化(
typed
)对象
在 Java 2 平台安全体系结构中,所有访问权限都是类型化的并且有层次结构,其根是抽象类 java.security.Permission 。通常一个 Permission 包含一个
目标(“由这个权限控制的操作将对谁执行?”)和一个操作 (“如果这个权限允许的话,对这个目标将执行什么操作?”)。
在允许一段运行的代码对特定的“目标”执行特定的“操作”这一上下文中,一个重要的概念是代码不一定被授予与所需要完全一样的 Permission 。相反,只要可以从实际授予这段代码的 Permission 中
推断出或者隐含了所需要的 Permission 就可以。例如,如果一段运行代码授予了读目录 /x 中所有文件的权限,那么它就不需要对目标文件 /x/in.xtx 执行 读操作的显式权限,因为前一个权限隐含了后者。
显然,某个 Permission 是否隐含另一个 Permission 的定义将取决于这两个 Permission 是如何定义的。至少,这两个 Permission 必须为同一类型。不过,不能指望运行时进行进一步的判断,并且必须将这种隐含推断逻辑指派给所涉及的 Permission 类。运行时通过调用一个恰当地取名为 implies 的方法来查询 Permission 类的隐含推断逻辑。
1.2
聚集的权限
新安全体系结构也引入了 聚集(aggregation)的概念。在 Java 2 平台上,可以聚集同一类型的 Permission 对象的多个实例。一组这种类型称为 PermissionCollection 。例如,一个 PermissionCollection 可能包含两个 java.io.FilePermission 实例,表示读取两个不同文件的特权。
这样的类型化对象干净地封装了创建和维护一个集合并遍历这一集合的功能。不用在每次要检查权限时对每一个对象分别调用 implies() 方法,Java 运行时只是调用由 PermissionCollection 对象提供的 implies() 方法并等待其响应。
可以为所创建的每一个自定义 Permission 对象定义一种新的 PermissionCollection 类型。当然, PermissionCollection 中 implies() 方法的具体实现取决于给定 Permission 对象的特性。
Permissions
对象
同类型的
Permission
的不同实例可以聚集到一个
PermissionCollection
的对象中,不同类型的
PermissionColletion
对象又可以被封装到
Permissions
对象中 。Permissions 类还提供了一个 implies() 方法。该方法首先在其内部集合中定位正确的 PermissionCollection 实例(那个包含一组正确类型的 Permission 对象的实例),然后调用由此获得的 PermissionCollection 对象的 implies() 方法,并向它传递要检查的 Permission 。
安全策略和保护域
适用于一个系统的安全策略实质上是一个良好定义的“仓库”,它存储了授予这个系统中不同实体的访问权限的断言。根据
保护域(
protection domain
)是由系统中当前获得授权的一个实体可以直接访问的一组对象所界定的 。 在此基础上构建的 Java 2 平台安全策略设计为根据 ProtectionDomain 授权访问权限,而不是向单个的一段运行代码授权这种权限。因此,每一个类或者对象“属于”一个 ProtectionDomain ,安全策略对这个保护域授予了某种访问权限。重申 ProtectionDomain 的观点,一个特定的 ProtectionDomain 封装了一组类(例如,所有从特定位置上装载、并用特定密钥签名的所有类),它们的实例将会授予同样的一组权限。 (
注:在
jaas
前,保护域自己就配置了权限!
Protection
包含了
grant
后面的
codebase
等东西,也包含
{permission p action
。。。。
}
等,用户的授权是给保护域的,所以,保护域可进行灵活配置 )。
这种间接性(即,权限不是直接授予类和对象)背后的理由是可扩展性
-- 它应当可以改变和/或细化构成 ProtectionDomain 的定义,而不会影响权限的授予。(确实,JAAS 之前的 ProtectionDomain 只由“属于”它的代码描述,而 JAAS 后的 ProtectionDomain 还由运行代码的、经过认证的用户描述。
由于每一位用户都分配到了设置了他或者她的权限的特定
ProtectionDomain
,进行用户认证就可以使给定的一段代码根据当前认证用户而用不同的一组权限运行 。)
保护域和代码源
显然,一定要能惟一地标识一段运行代码以保证它的访问权限没有冲突。运行代码的惟一标识属性共有两项
:代码的来源(代码装载到内存所用的
URL
)和代码的
signer
实体 (由对应于运行代码的数字签名的一组公共密钥指定)。这两种特性的组合在 Java 2 平台安全体系结构中编写为给定运行代码的 CodeSource 。现在可以提供 ProtectionDomain 的更严格定义了:
ProtectionDomain
是一组
CodeSource
及其访问权限
Permissions 。换一种说法, ProtectionDomain 表示授予特定 CodeSource 的所有权限。
Java 运行时通过名为 java.security.Policy 的类(的具体扩展)设置 ProtectionDomain 与授予它的权限之间的映射。这个类的默认扩展是 sun.security.provider.PolicyFile 。正如其名字所表明的, sun.security.provider.PolicyFile 从一个文件中获得 CodeSource (由位置 URL 和 signer 标识别名)与授予它的权限之间的映射。可以通过环境变量 java.security.policy 将这个文件的位置作为输入提供给 JVM。 Policy 类提供了一个名为 getPermissions() 的方法,可以调用它以获得授予特定 CodeSource 的一组权限。
SecureClassLoader
一个类与 其 ProtectionDomain 之间的映射是在类第一次装载时设置的,并在类被垃圾收集之前不会改变。一个类通常是由一个名为 SecureClassLoader 的特殊类装载的。 SecureClassLoader 首先从相应 URL 处装载字节,如果需要还会验证包围文档文件的数字签名。然后它调用上述 getPermissions() 方法获得授予类的 CodeSource (
一个类属于一个
CodeSource )的一个填充了静态绑定权限的异类 PermissionCollection 。然后 SecureClassLoader 以
CodeSource 和
PermissionCollection 创建新的 ProtectionDomain。 最后,用装载的类字节向 JVM 定义一个类,并在关联的 ProtectionDomain 中维护一个引用指针。
默认情况下,会创建一个 ProtectionDomain ,并作为“特殊”情况处理,即属于这个域的代码被认为是受信任的并可以获得特殊的权限。这称为 系统域并包括由 系统(应用程序)装载器、扩展装载器和 bootstrap 装载器装载的类。(有关 Java 类装载器的更多信息请参阅 参考资料。)
动态权限
直到 Java 平台 1.3,都只能用(上面描述的)以 CodeSource 和相关权限为参数的构造函数创建 ProtectionDomain 。这意味着授予特定 ProtectionDomain 的权限必须在构建时就已经知道,并且没有动态刷新所授予的一组权限的灵活性。然而在 Java 2 平台 SDK 1.4 中, ProtectionDomain 可以同时封装(通过其构造函数传递的)静态权限和动态权限。
动态权限是在权限检查时由生效的策略所授予的、并由 ProtectionDomain 隐式地处理。对 ProtectionDomain 调用 implies() 方法时(实质上是对权限进行检查时),它调用安装的 Policy 类的 getPolicyNoCheck() 方法。因而 Policy 类提供了
刷新所授予的一组权限并向调用
ProtectionDomain
返回这个刷新的权限的可能。这保证了针对在构造时提供的
PermissionCollection
和在那一瞬间绑定的
Policy
的组合进行权限检查 。
运行时访问检查
由一个名为 SecurityManager 的类负责实施系统安全策略。在默认情况下不安装安全管理器,必须通过一个在启动时传递给 JVM 的、名为
java.security.manager 的环境变量显式地指定。任何应用程序都可找到安装的 SecurityManager 并调用它相应的
check<XXX> 方法。如果所要求的权限在给定运行时上下文中是授予的,那么调用将无声地返回。如果权限没有授予,那么将抛出一个 java.security.AccessControlException 。
在 Java 1.1 的时代, SecurityManager 通过其内部逻辑负责管理所有权限本身。因此,任何需要自定义逻辑进行访问决定的应用程序都必须实现并安装一个自定义的 SecurityManager 。Java 2 平台安全体系结构通过引入一个名为 AccessController 的新类使这一切变得简单了,并更具有可扩展性。这个类的目的与 SecurityManager 是一样的,即它负责做出访问决定。当然, 为了向后兼容性保留了 SecurityManager 类,但是其更新的实现委派给了底层的 AccessController 。对 SecurityManager 类进行的所有 check<XXX> 方法都解释为相应的 Permission 对象,并将它作为输入参数传递给 AccessController 类的 checkPermission() 方法。
Java
程序中的执行线程
在 Java 程序的执行过程中,可能需要在不同的时间访问“受保护的”资源。当我谈到执行 Java 程序时,我的意思是在特定类 C 1 中(因而在特定的方法中,如 main() )中启动、通过类 C 2 到 C n-1 、并“结束”于 C n 的执行线程。下面是一个 Java 程序执行的典型控制流程:
调用类 C 1 的 main() 方法 -> C 1 的 main() 方法调用 C 2 的 m C2 方法 -> C 2 的 m C2 方法调用 C 3 类的 m C3 方法 -> ... -> 类 C n-1 的 m Cn-1 方法调用类 C n 的 m Cn 方法。
假定方法 m Cn 必须访问一个受保护的资源以完成其功能,它调用系统中生效的 AccessController 以确认是否可以继续请求的对特定“受保护的”资源的访问。如果 AccessController 同意放行,那么就执行所要求的操作,控制返回给调用者( C n-1 类的 m Cn-1 方法),它又将控制返回给其调用者( C n-2 类的 m Cn-2 方法),如此继续。
在 JVM 中,线程的控制流表示为 帧堆栈(stack of frame)。每个帧基本上维护有关特定 m Ck 方法、它的类 C k 以及这个方法调用的变量/参数的信息。图 1 显示了一个典型的调用堆栈。
图 1. 典型调用堆栈的屏幕快照(
cn
先于
cn-1
返回 )
上面堆栈中的每个类属于一个
ProtectionDomain 。 一般来说,这样遍历的一组 ProtectionDomain 将包含 <=n个元素。像图 1 显示的这样一个调用堆栈快照将编写为(codified)为一个 AccessControlContext 并由 AccessController 对象提供的本机方法调用返回。
访问检查内幕
最后得到的一组适用权限的算法是要计算所有权限的交集。换句话说,某一权限,只有与这个特定瞬间、这个线程的执行堆栈上出现的所有类 C i 相应的 ProtectionDomain 相 关联时,这个权限才适用于给定的执行线程。(注;只有所有的ProtectionDomain允许,才能允许)
访问控制方法
确定权限集的交集的算法是在 AccessController 类的 checkPermission 方法中间接实现的。本质上,调用这个方法所发生的事情是对那一瞬间调用堆栈和一组相互交叠的权限进行快照。所请求的权限必须包含在交集结果中或者是它所隐 含的。如果这种检查判断为 true,那么 checkPermission() 方法就安静地返回,如果不是,那么就抛出一个异常。(显然, 图 1中描述的调用堆栈中最后一帧实际上是对 AccessController 类的 checkPermission() 方法的调用。)
注意,直到现在我还没有提到图 1 中描述的调用堆栈的线程起源。这个线程 T 2 可能是由另一个线程 T 1 在其调用堆栈中的某一点上创建的,只要 JVM 为在系统中执行的每一个线程维护单独的调用堆栈。可以直观地假定, T 2 将继承 T 1 调用堆栈(不过只是 T 1 已经运行的那部分)以保证继承的 ProtectionDomain 的权限集也与 T 2 自己的调用堆栈的 ProtectionDomain 取交集。这将保证子线程(这里是 T 2 )不会偷偷地得到它的父线程(在这里是 T 1 )所拒绝的某个权限。
跨域调用问题
如果属于能力更低的域的类调用属于能力更高的域中的类,就有可能出现奇怪的现象。能力更高的域(类),例如 C n 拥有可以访问所需要的“受保护的” 资源的权限,如果它是由没有相关权限的、能力更低的域(类) C n-1 所调用的,它就不能访问这些资源了。如果 C n
一定要 访问受保护的资源才能工作怎么办(注:如写日志)?不应当有这样一种机制吗
:在确定有效的权限集,让
C n
可以告诉安全系统忽略其调用者(及调用者的调用者,并上推到调用堆栈最上层的类)的权限。(注:就是从我查起的意思)
现在,Java 2 平台安全体系结构提供了一种机制,提供的就是这种功能。 AccessController 类有一个名为 doPrivileged 的方法(实际上提供了这个方法的许多变种,但是基本思路是相同的),它用特殊的旗标标记调用堆栈中有关的帧。
在这个执行线程中调用 checkPermission 方法时,只有它和它下面出现的类的权限集才会取交集。调用类和它的上级(即所有在它 上面的堆栈帧)的权限集都 不包括在交集计算中。
不难看出为什么要包括 在调用堆栈以下发生的所有类的权限集:需要考虑属于能力更高的域的类调用属于更能力更低的域的类的情况。更明确地说,需要防止能力更高的域 ( C k ) 将其额外的能力传递给能力更低的域 ( C k+1 )。
doPrivileged 方法的所有变种都以一个类型为 PrivilegedAction 的对象作为输入。这个对象必须有一个名为 run() 的方法,在调用堆栈中的当前帧特别做了如上所述的标记时,由运行时执行这个方法。
因此,任何时候如果有一些代码,希望在执行时让它的权限
临时性地授予给调用堆栈帧前面的代码时(注:即
Ck-1
具有
Ck
的权限),必须将代码包装为
PrivilegedAction
的形式并用这个对象作为输入调用
AccessController
的
doPrivileged()
方法。
积极访问检查与懒惰访问检查
访问检查算法一直被描述为计算调用堆栈上所有 ProtectionDomain s 的权限集的交集。可以用积极(eager )方式或者懒惰(lazy)方式计算这种交集。有关这两种方法的细节,请参阅所附的 Eager versus lazy access checks sidefile。
调用堆栈优化
在 图 1中看到的调用堆栈快照(或者 AccessControlContext )是在对 AccessController 进行 checkPermission 调用时获得的。在内部, AccessController 在确定这个调用堆栈时进行一些优化,以使访问检查循环尽可能地快。这些优化包括:
返回的 ProtectionDomain 只到达(并包括)通过调用 AccessController 的 doPrivileged 特别标记的第一个堆栈帧。从前面对 doPrivileged 调用的讨论中显然可以看出这样做的原因。
返回的 ProtectionDomain s 不包括系统域。系统域定义为具有所有权限,所以不需要检查是否“隐含”了所需要的权限(它总是隐含的)。
返回的 ProtectionDomain 都是惟一的(即如果多个堆栈帧对应于同一个 ProtectionDomain ,那么只会返回一个 ProtectionDomain )。
如果搜索完当前 AccessControlContext 并且没有抛出 AccessControlException ,那么将对这个线程在创建时从其父线程“继承”的 AccessControlContext 进行同样的搜索( AccessControlContext 被继承,即一个孙子线程将继承它的所有上级的调用堆栈)。
doPrivileged() 方法的变种
在前面看到调用
AccessController
的
doPrivileged()
方法是用一个特殊旗标标识调用堆栈的当前帧,指明控制流中所有前面的帧都不进行访问检查 。 还看到调用堆栈快照(或者 AccessControlContext )是在对 AccessController 进行 checkPermission 调用时获得的。不过,这个 AccessControlContext 不一定就是应当用来确定是否授予所请求的权限的那一个 。 例如,请求可能是由客户机发起并发送给服务器进行处理。服务器通常代表客户机执行请求实施代码。
因为服务器的一部分用于完成请求,如果它调用
AccessController
,那么返回的调用堆栈将是服务器的 。 显然,不希望(只) 使用服务器的 AccessControlContext 给客户机授权。(当然,希望保证服务器代码本身对试图访问的资源有相应的权限,不过更重要的是保证客户机对服务器代表它访问的资源有相应的权限)。服务器 运行时通常是已经授予了权限,因此,真正希望使用的是在客户端向服务器发送请求时存在的客户端调用堆栈。
AccessController 类提供了 doPrivileged() 方法的另一个变种,它以 AccessControlContext 的实例作为输入。假定客户机设法获得了其 AccessControlContext 的一个副本( AccessController 类提供了实现这个目的的方法)并将它传递给服务器,服务器可以通过调用以从客户端获得的上述 AccessControlContext 作为输入的 doPrivileged ,将请求的完成代码作为 PrivilegedAction 执行。
在这种情况下,权限检查的算法(假定在过程某处,
AccessController 在 对 PrivilegedAction 的 run() 方法调用后,调用了 checkPermission 时)通过执行上述的循环推进,直到在堆栈中遇到了特别标记的帧,这时,调用作为输入传递的 AccessControlContext 对象的 checkPermission() 方法。这个调用实质上会执行同一个算法,但是是对于在这个 AccessControlContext 中封装的调用堆栈(属于客户机)执行。
为何要使用以用户为中心的授权?
Java 2 平台安全体系结构的以代码为中心的授权基于这样的假设,即必须保护用户不受外界影响。为了保证恶意 Java 程序(由世界上恶意破坏者编写的)不会损坏用户的系统,所有移动代码都视为不受信任的,并且那怕进行最无害的操作也要求具有特殊的访问权限。
相反,JAAS 的以用户为中心的认证模型是以保护世界不受用户影响的思路开发的。随着越来越多的移动和企业网络的出现, 信任概念有了不同的定义。在现实生活中,如果我信任某人 X 多于信任任何某人 Y,我将允许 X 有比 Y 更多的自由度。与此类似,如果一个 Java 应用程序将由多位用户使用(其中一些人实际上可能是恶意破坏者),那么最好将访问权限扩展为以 每个用户为基础。在这种新模型下,根据每位用户受信任的程度,对他或者她授权使用应用程序的某一范围的功能。
在下面一节中,我将重点介绍 Java 认证和授权服务(Java Authentication and Authorization Service)的以用户为中心的授权模型。虽然 JAAS 代表了 Java 平台安全体系结构的价值的翻天覆地的变化(即它从基于代码的模型转移到以用户为基础的模型),但是您会看到它的许多组件是熟悉的,尽管它们已经更新过以满 足新的要求。
JAAS
授权体系结构
既然 JAAS 的目的是为了以每位用户为基础控制任何一段代码所能做的事情,
因此需要首先能够准确和惟一地标识用户,换句话说,必须能够对他们进行认证。虽然在这里我不会在 JAAS 的“认证”方面花很多时间(有关这个主题的更多参考请参阅 参考资料),但是我将重点介绍它的一个核心组件: Subject 类。
基于
subject
的访问控制
Subject 类用于表示在给定系统中认证的用户(
即填充的
Subject
是
JAAS
认证过程的结果 )。 在内部, Subject 包含一组 Principal 对象(和其他有关用户的信息),其中每个 Principal 对象表示同一个用户的不同“身份”。例如,一个 Principal 可能是我在一个终端系统上的用户 ID,而另一个可能是我在同一系统上所属于的“组”。
在前面我介绍过 生效的 Policy 是如何在系统中设置 ProtectionDomain (以及由相关的 CodeSource 标识的、“属于”它的类)和授予它的权限之间的映射的。JAAS 通过要求用一组 Principal 进一步描述 ProtectionDomain (超越了 CodeSource )而强化了这种概念
。当系统
Policy
设置了这样的
ProtectionDomain
(即除了
CodeSource
,还用一组
Principal s
描述)和授予它的权限之间的映射后,如果要用
ProtectionDomain
的权限来检查是否应当授予用户某个请求的权限,那么在
Subject
中包含的、与运行这段代码的认证用户相对应的
Principal
对象必须匹配在这个
ProtectionDomain
中包含的
Principal
对象 。
既然 Java 2 平台已经有了干净的、高效的、使用调用堆栈(通过 AccessControlContext )的授权实现,那么保持它就容易得多了,只要提供一种机制将运行这段代码的用户的身份(如由用户的 Subject 所提供的)“注入”到在权限检查瞬间调用堆栈中的 ProtectionDomain 。
为此,JAAS Subject 类提供了两个静态方法,称为 doAs 和 doAsPrivileged 。 这些方法期待的
输入是认证的用户的 Subject 实例和 PrivilegedAction 的一个实例(注:
以这样的身份执行
PrivilegedAction )。 基本思路是应用程序应当首先认证用户,对认证的用户建立了 Subject 后,这个用户可能希望执行的每一个操作都包装为 PrivilegedAction 、并由应用程序作为 Subject (就像方法自己的名字所表明的 -- doAs() !)执行。这两个方法之间有细微但是重要的区别,我们将在稍后介绍。
为了能够将操作作为 Subject 执行,必须在调用堆 栈中将 Subject 引入(或者注入) ProtectionDomain 。这是在一个名为 DomainCombiner 的专用接口的帮助下实现的,我将在开始 doAs() 和 doAsPrivileged() 方法的内幕之前介绍这个接口。
DomainCombiner
如前所述,对于一个 AccessControlContext (一个调用堆栈),在 JAAS 中将 Subject 注入堆栈中的 ProtectionDomain 是通过实现 DomainCombiner 接口(一个特定的实现是 SubjectDomainCombiner )所处理的。
(注:
AccessContoller
在
CheckPermissons
时,生成
context
,而在生成时,通过
SubjectDomainCombiner
完成
subject
的注入,所以,通过验证后的
subject
是动态注入的)
注入是在将 SubjectDomainCombiner 作为构造函数参数传递以构建 AccessControlContext 时执行的。(作为参数传递给 doAs 调用的 Subject 被封装到 SubjectDomainCombiner 对象中,这种封装是在创建后者时,将 Subject 作为构造函数参数传递而完成的。)不过,真正的工作是在 SubjectDomainCombiner 的 combine() 方法中完成的。您将在稍后看到在这个方法中所发生的过程。
Subject.doAs()
方法
应用程序可能期待在认证用户之后调用 Subject.doAs() 方法(即,当 Subject 对用户是可用的时)。在内部,这个调用会产生下列活动:
1. 通过调用 AccessController 的 getContext() 方法获得当前执行线程的 AccessControlContext 。注意,这个调用堆栈当然将会按前面描述的过程优化。
2. 创建封装了认证的 Subject 的 SubjectDomainCombiner。
3. 用第 1 步的 AccessControlContext 和第 2 步的 SubjectDomainCombiner 创建 AccessControlContext 对象。
4. 调用 AccessController 的 doPrivileged() 方法,将第
2
步创建 了 AccessControlContext 的 PrivilegedAction 实例 (下面称为“ privileged AccessControlContext ”) 作为参数传递给它。(
注:
AccessControlContext .createContext()
调用了
AccessController.doPrivileged().
)
5. 运行时在内部保存 privileged AccessControlContext(注:
标记后的
AccessControlContext
,应该是关键,但原文没提 ) 并执行 PrivilegedAction 对象的 run() 方法。如前所述,在要访问受保护的资源时,需要调用 AccessController 类的 checkPermission() 方法。
6. 在 内部,这个调用让 AccessController 寻求当前调用堆栈(即 AccessControlContext )。运行时将返回包含第 4 步介绍的 privileged AccessControlContext 的 AccessControlContext 。
7. 如前所述,在检查 AccessControlContext 的帧的 ProtectionDomains 是否允许所要求的权限之前,必须优化它。作为这个优化过程的一部分,要求封装在 privileged AccessControlContext (
注:调用
doAs
前的堆栈 )中的 SubjectDomainCombiner 结合当前在调用堆栈上(
注:调用
doAs
后的堆栈 )的 ProtectionDomains 和在 privileged AccessControlContext 中出现的 ProtectionDomains 。结合过程如下:
A首先,优化 privileged AccessControlContext 的 ProtectionDomain 以删除所有 系统和重复的域。
B然后,优化当前调用堆栈上的 ProtectionDomain 以删除系统域以及已经出现在 privileged AccessControlContext 中的域。这时,得到的两组 ProtectionDomain 就都没有系统域并且只包含不相同的域。
C
对于从第
b
步得到的每一个
优化的
ProtectionDomain
,创建一个新的
ProtectionDomain
,它复制了原来的属性如
CodeSource
和
Permission
,而且还包含一组与在这个
SubjectDomainCombiner
中包含的
Subject
相关的
Principal 。
(注:在这里,
subject
进入到调用
doAs
时的调用站)
D将优化的 ProtectionDomain (从第 a 步得到的)附加到新创建的 ProtectionDomain 上(从第 c 步得到的)。用这些结合的 ProtectionDomain 和 SubjectDomainCombiner 创建一个新的 AccessControllerContext 并返回它。
8. 现在有了一个优化的 AccessControlContext (其中这个 Subject 的一组 Principal 与当前调用堆栈中的每一个 ProtectionDomain 相关联),可以安全地调用它的 checkPermission() 方法。
9. 对 checkPermission() 方法的调用使得运行时在如前所述的循环中遍历包含在这个 AccessControlContext 中的一组 ProtectionDomain ,并检查每一个 ProtectionDomain 是否隐含所要求的 Permission 。这里值得注意的一个事实是检查的一组 ProtectionDomain 将包括
当前调用堆栈的
ProtectionDomain (已经与在认证的 Subject 中包含的 Principal 相关联)和
privileged AccessControlContext
中的
ProtectionDomain
(在调用
doAs()
方法之前的调用堆栈), 它还没有与包含在认证的 Subject 中的 Principal 相关联。所请求的 Permission 必须由所有这些 ProtectionDomain 隐含。
调用 Subject.doAs() 方法的另一个效果是:可以通过 PrivilegedAction 的 run() 方法达到的任何代码都可以使用认证用户的身份(即 Subject )。得到 Subject 的方法如下:
1. 通过调用 AccessController 的 getContext() 方法得到当前 AccessControlContext 的句柄。在内部,这个方法以上面第 7 步同样的方式返回一个优化的 AccessControlContext 。
2. 调 用 Subject 类的 static getSubject() 方法,将上面获得的 AccessControlContext 作为输入参数传递。在内部,在进入下一步之前,它检查调用者是否有 getSubject() 方法的 javax.security.auth.AuthPermission。
3. 在内部,这个调用提取包含在 AccessControlContext 中的 SubjectDomainCombiner ,从提取的 SubjectDomainCombiner 中提取出 Subject 并返回它。
这样返回的 Subject 表明了认证用户的身份,可以用于登录和/或数据级的授权等。
Subject.doAsPrivileged()
方法
像在 doAs() 方法中看到的那样,在调用 doAs 之前,请求的 Permission 必须由出现在调用堆栈中的 ProtectionDomain s 所隐含。由于现在已经熟悉的原因,可能不总是希望是这种情况。
正如在讨论 AccessController 类的 doPrivileged() 方法(这个方法以一个 AccessControlContext 为参数用于权限检查)的变种时提到的,
PrivilegedAction 可能实际上
表示一些服务器代表客户机 执行的一些操作(更准确地说是作为客户机,即好像假定服务器具有它代表其执行操作的客户机的身份,注:
PrivilegedAction
为客户代码 )。
在这种情况下,在调用
doAs
之前调用堆栈的快照将包含服务器的内部代码的
ProtectionDomain
,而让这些
ProtectionDomain
必须隐含一个任意请求的
Permission
显然没有意义 。(注:即priviligedContext应该不需检查)然而,所希望的是以下两种情况之一:
第 I 种情况: 应当用在客户端调用堆栈上的 ProtectionDomains (当客户机向服务器发送请求的瞬间的快照)来检查请求的 Permission (以及与用户身份相关联的服务器端调用堆 栈 ProtectionDomain )。 (
即,检查
doAs
之后的服务端
context
以及客户
Context
)
第 II 种情况:应当只用与用户身份相关联的服务器端调用堆栈 ProtectionDomain 进行权限检查。 (
即,只检查
doAs
之后的服务端
context
)
这个工具是通过 Subject 类的 static doAsPrivileged() 方法提供的。
这个方法以一个
Subject
和一个
PrivilegedAction
作为输入参数(就像
doAs()
方法),不过,它还有一个
AccessControlContext
参数。这样,客户机可以安排取它自己的
AccessControlContext
快照并发送给服务器,这样就可以将它传递
给
doAsPrivileged
调用。这样可以处理上面第
I
种情况。否则,可以传递
null
代替
AccessControlContext
调用
doAsPrivileged
,这样可以处理上述第
II
种情况。
在内部, doAsPrivileged() 方法的步骤如下:
创建一个中间的 AccessControlContext ,它指向传递的 AccessControlContext (如果它是非 null 的),或者为 null 时指向一个新创建的 AccessControlContext (有一个空的 ProtectionDomain 列表)。
从第 2 到 9 步之间的所有步骤都与以前一样。应当已经很清楚了,达到第 9 步时,最终将用于权限检查的这些 ProtectionDomain 将是已经注入认证用户的 Principal 列表的服务器调用堆栈的 ProtectionDomain 加上客户调用堆栈的(未改变的)
ProtectionDomain 的组合,或者是已经注入认证的用户的 Principal 列表的服务器端调用堆栈上的 ProtectionDomain 。这就是您要实现的。
授权模型的矛盾
我在这篇导游中讨论了 Java 授权内幕的大量基础内容。介绍了原来 Java 2 平台安全体系结构的基于代码的授权模型和在 JAAS 中引入的基于用户的授权框架。在本导游的最后一程,将介绍 JAAS 认证模型中的一个矛盾,并且我将描述一个解决它的实际方法。
嗨,我的
Subject
到哪里去了?
假设应用程序认证了用户并为她设置了一个 Subject 。用户请求某个功能,于是应用程序调用 doAsPrivileged() 方法并传递认证后的 Subject 和所需功能的 PrivilegedAction 。传递的 AccessControlContext 为 null
,保证只对调用堆栈中调用
doAsPrivileged
之后的
ProtectionDomain
进行权限检查。
考虑执行 PrivilegedAction 实例的 run() 方法。可以从前面看到,在这个 PrivilegedAction 中的一段代码应当可以请求并得到认证的 Subject 。现在假定在这个方法中的控制流中某个地方,调用了 AccessController 的 doPrivileged() 方法(特别是只接受 PrivilegedAction 实例的 doPrivileged )和在这个(嵌入的) doPrivileged 调用中执行的 PrivilegedAction 也需要提到认证用户的身份。
与以前一样,第一步是通过调用 AccessController 的 getContext() 方法得到当前 AccessControlContext 的句柄。如在前面讨论 Subject.doAs() 方法时所说,与当前调用堆栈一同返回的还有一个 privileged AccessControlContext (包含封装了认证的 Subject 的 SubjectDomainCombiner ),所以优化过程可以实际上将一组 Principal 从 Subject 注入到最后一 组 ProtectionDomain 列表中。不过,因为对 AccessController 的 doPrivileged() 方法进行了新的调用,分配了一个新的 privileged 元素,和用这个元素更新的当前执行线程作为最高层的 privileged 元素。因为没有向 doPrivileged 调用传递 AccessControlContext ,所以这个 privileged 元素没有任何 privileged AccessControlContext 与之相关联,这与前面提到的情况不一样。对 getContext 的调用返回直到这个最高 privileged 元素的调用堆栈,因此,有关认证的 Subject 信息在这个执行期间是不可用的。
当然,一旦 inner PrivilegedAction 执行完,这个 privileged 元素就弹出堆栈,而对 getContext 的所有调用都会再返回包含 privileged AccessControlContext 的 AccessControlContext (它又包含封装了认证 Subject 的 SubjectDomainCombiner )。因此,当从 Subject.doAs() 方法中调用的 PrivilegedAction 完成后,将可以再次获得 认证的 Subject 。
实用解决
方法
一种解决这个问题的方法是创建一个自定义 SubjectHolder 类,它包装了一个 static ThreadLocal 以存储当前 Subject 。 认证的 Subject 可以在认证之后和调用 doAs() 方法之前存储在这个 SubjectHolder 中。这之后,所有执行的代码(直接或者间接,不管是否包装在另一个 PrivilegedAction 中)都将可以得到认证的 Subject ,只要让 SubjectHolder 返回 ThreadLocal 变量的内容。
WebSphere 应用服务器提供了一个这种解决方法的例子。该应用服务器提供了一个帮助器类 WSSubject ,它有 static doAs() 和 doAsPrivileged() 方法,它们具有相同的 Subject 类签名。在调用相应的 Subject.doAs() 方法之前, WSSubject.doAs() 方法基本上将用户凭据与当前执行线程(可以用于 Enterprise JavaBean (EJB)调用)相关联。在离开 WSSubject.doAs() 方法时,恢复原来的凭据并与执行线程相关联。
结束语
本文深入分析了 Java 2 平台安全体系结构和 JAAS 的 Java 授权。完成本文(或者游览)后,应当可以对每一种授权框架的基础概念及它们的底层机制有全面的了解。
Java 2 平台安全体系结构和 JAAS 共同构成了当前的 Java 授权模型。我介绍了 JAAS 授权模型中的一个矛盾之处,并描述了一种解决它的实用方法,并提供了它的现实世界实现的一个例子。
参考资料
1. 您可以参阅本文在 developerWorks 全球站点上的 英文原文.
2. Daniel Wallach 和 Edward Felten 的“ Understanding Java Stack Inspection” (IEEE Symposium on Security and Privacy,1998 年 5 月)提供了在本文中描述的基于堆栈检查的访问控制算法的形式分析。
3. 在本文中我完全侧重于 JAAS 的 授权部分。可以通过 Sun Microsystems 的 JAAS Authentication Tutorial了解有关 JAAS 的认证部分的更多内容。
4. 请参阅 “ java安全性 第二部分 认证与授权” ( developerWorks,2002 年 7 月)上对一般的 Java 安全性以及具体的 JAAS 认证和授权的很好介绍。
5. 如要以更实际的方式理解 JAAS,可以试试“ 扩展JAAS实现类实例级授权” ( developerWorks,2002 年 4 月)
6. 对于 J2EE Web 应用程序安全体系结构的企业级分析,请参阅 Kyle Gabhart 的“ J2EE 探索者: 用 JAAS 和 JSSE 实现Java 安全性”( developerWorks,2003 年 11 月)。
7. 可以通过 Greg Travis 的“ Using JSSE for secure socket communication”( developerWorks,2002 年 4 月)学习更多关于 Java 安全套接字扩展的内容。
8. Stuart Halloway 在他的 Component Development for the Java Platform (Addison-Wesley,第一版,2001 年 12 月)一书中提供了对安全类装载器的完整介绍。
9. Jerome Saltzer 和 Michael Schroeder 的“ The Protection of Information in Computer Systems” (Proceedings of the IEEE,1975 年 9 月)是信息系统安全历史上的一篇经典文章。
10. 在 developerWorks Java 技术专区将会发现关于 Java 编程各个方面的文章。
11. 请访问 Developer Bookstore,获得技术书籍的详细列表,其中包括数百本 Java 相关的图书。
12. 还可以参阅 Java 技术专区教程主页,从 developerWorks 获得免费的 Java 相关教程的完整列表。
关于作者
Abhijit Belapurkar 从印度德里市的印度理工学院获得了计算机科学方面的技术学士学位。他在分布式应用程序的体系结构和安全方面拥有近 10 年的经验,并且在使用 Java 平台构建 N-层应用程序方面拥有 5 年以上的经验。他目前在印度班加罗尔市的 Infosys Technologies Limited 担任 J2EE 方面的高级技术架构师。