在信息安全性领域,授权是世界的的中心,因为它是控制个体(即人、进程和计算机)对系统资源的访问权限的过程。直到最近,在 Java 安全体系结构中相关的问题都是“这段运行中的代码的访问权限是什么?” 随着 Java 认证和授权服务(Java Authentication and Authorization Service,JAAS)的引入,这种情况改变了。JAAS 首先是作为 JDK 版本 1.3 的平台扩展,之后作为 JDK 1.4 及以后版本的核心部分。在 JAAS 中,相关问题变成了“运行这段代码的认证用户的访问权限是什么?”
在本文中,将同时介绍老的以代码为中心的 Java 授权体系结构和新的以用户为中心的体系结构。我将首先对 Java 2 平台安全体系结构作一概述,重点放在这个体系结构如何利用两个基本概念 -- 安全策略和保护域 -- 来定义、组织和聚集静态和/或动态访问权限。然后详细分析 Java 2 平台安全体系结构的运行时访问检查功能的底层机制,包括堆栈检查和确定是否授予权限的遍历(traversal)机制。在了解了以代码为中心的授权模型是如何工作的后,我将转向 Java 授权和认证服务(JAAS)的以用户为中心的授权模型。在这里,我将重点放到基于 subject 的访问控制这一概念上,并展示在 JAAS 中,它是如何在原来 Java 2 平台安全体系结构的堆栈检查机制之上实现的。
注意,本文假定读者熟悉 Java 平台(J2SE SDK 1.4)上的应用程序编程,以及企业应用程序安全性的基本概念。与 Java 平台的以代码为中心和以用户为中心的授权体系结构的所有概念都只作为介绍。
以代码为中心的授权
Java 平台传统上是用来运行移动代码的,如 applet。为了保护系统资源不被这些从网络上下载到用户浏览器中的任意代码片段所破坏,applets 被限制到一个沙箱中,它们在这里以有限的一组权限运行 。另一方面,对于本地 Java 应用程序,很少会(如果会的话)安装将提供类似沙箱环境的安全管理器。因此,本地应用程序通常受到信任可以访问所有系统资源。
JDK 1.x 模型和 Java 2 平台 SDK 版本 1.2 的新安全结构之间的最大区别是引入了新的、可配置的安全策略,这样就可以实现细化的和可管理的访问控制。所有代码(不管是本地还是下载的,不管是签名或者没有签名的)都可以受到定义良好的安全策略的约束,它为不同的代码授予(可能是重叠的)权限。同时,随着在 JVM 中引入了多进程能力(请参阅 参考资料),出现了对基于用户的访问控制的要求。
Java 2 平台安全体系结构背后的基本原理可以总结如下:一个系统级的 安全策略定义了按 保护域组织的执行代码的 访问权限(按照应用程序的需要)。安全策略用于访问控制检查,这是由 JVM 在运行时执行的。在本次导游中,我将逐一详细阐述这些概念。
访问权限作为类型化(typed)对象
在 Java 2 平台安全体系结构中,所有访问权限都是类型化的并且有层次结构,其根是抽象类 java.security.Permission
。通常一个Permission
包含一个目标(“由这个权限控制的操作将对谁执行?”)和一个操作(“如果这个权限允许的话,对这个目标将执行什么操作?”)。
在允许一段运行的代码对特定的“目标”执行特定的“操作”这一上下文中,一个重要的概念是代码不一定被授予与所需要完全一样的 Permission
。相反,只要可以从实际授予这段代码的 Permission
中推断出或者隐含了所需要的 Permission
就可以。例如,如果一段运行代码授予了读目录 /x 中所有文件的权限,那么它就不需要对目标文件 /x/in.xtx 执行 读操作的显式权限,因为前一个权限隐含了后者。
显然,某个 Permission
是否隐含另一个 Permission
的定义将取决于这两个 Permission
是如何定义的。至少,这两个 Permission
必须为同一类型。不过,不能指望运行时进行进一步的判断,并且必须将这种隐含推断逻辑指派给所涉及的 Permission
类。运行时通过调用一个恰当地取名为 implies
的方法来查询 Permission
类的隐含推断逻辑。
聚集的权限
新安全体系结构也引入了 聚集(aggregation)的概念。在 Java 2 平台上,可以聚集同一类型的 Permission
对象的多个实例。一组这种类型称为 PermissionCollection
。例如,一个 PermissionCollection
可能包含两个 java.io.FilePermission
实例,表示读取两个不同文件的特权。
这样的类型化对象干净地封装了创建和维护一个集合并遍历这一集合的功能。不用在每次要检查权限时对每一个对象分别调用 implies()
方法,Java 运行时只是调用由 PermissionCollection
对象提供的 implies()
方法并等待其响应。可以为所创建的每一个自定义 Permission
对象定义一种新的 PermissionCollection
类型。当然, PermissionCollection
中 implies()
方法的具体实现取决于给定 Permission
对象的特性。
Permissions 对象
除了对特定的 Permission
类型有多个实例,任何给定的一段运行代码都将得到不同类型的 Permission
。 Java 2 平台安全体系结构为此以Permissions
对象的形式提供了一组 PermissionCollection
对象。一个 Permissions
对象是单个 Permission
实例的 集合的 集合。Permissions
类还提供了一个 implies()
方法。不过,要记住为了让一个 Permission
隐含另一个,它们必须是同一类型的。因此,调用Permissions
对象的 implies()
方法会使后者首先在其内部集合中定位正确的 PermissionCollection
实例(那个包含一组正确类型的Permission
对象的实例),然后调用由此获得的 PermissionCollection
对象的 implies()
方法,并向它传递要检查的 Permission
。
安全策略和保护域
适用于一个系统的安全策略实质上是一个良好定义的“仓库”,它存储了授予这个系统中不同实体的访问权限的断言。根据 保护域(protection domain)的经典定义(请参阅 参考资料),域是由系统中当前获得授权的一个实体可以直接访问的一组对象所界定的(按照这个定义,实际上可以将 JDK 版本 1.1 中的 Java 沙箱想像为一个具有固定边界的保护域)。在此基础上构建的 Java 2 平台安全策略设计为根据ProtectionDomain
授权访问权限,而不是向单个的一段运行代码授权这种权限。因此,每一个类或者对象“属于”一个 ProtectionDomain
,安全策略对这个保护域授予了某种访问权限。重申 ProtectionDomain
的观点,一个特定的 ProtectionDomain
封装了一组类(例如,所有从特定位置上装载、并用特定密钥签名的所有类),它们的实例将会授予同样的一组权限。
这种间接性(即,权限不是直接授予类和对象)背后的理由是可扩展性 -- 它应当可以改变和/或细化构成 ProtectionDomain
的定义,而不会影响权限的授予。(确实,JAAS 之前的 ProtectionDomain
只由“属于”它的代码描述,而 JAAS 后的 ProtectionDomain
还由运行代码的、经过认证的用户描述。由于每一位用户都分配到了设置了他或者她的权限的特定 ProtectionDomain
,进行用户认证可以使给定的一段代码根据当前认证用户而用不同的一组权限运行。我将在讨论 JAAS 授权体系结构时,对所有这些内容给予更详细的描述。)
保护域和代码源
显然,一定要能惟一地标识一段运行代码以保证它的访问权限没有冲突。运行代码的惟一标识属性共有两项:代码的来源(代码装载到内存所用的 URL)和代码的 signer 实体(由对应于运行代码的数字签名的一组公共密钥指定)。这两种特性的组合在 Java 2 平台安全体系结构中编写为给定运行代码的 CodeSource
。现在可以提供 ProtectionDomain
的更严格定义了: ProtectionDomain
是一组 CodeSource
及其访问权限。换一种说法, 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
的一个填充了静态绑定权限的异类 PermissionCollection
。然后SecureClassLoader
创建新的 ProtectionDomain
,传递 CodeSource
及其相关的权限作为其构造函数的参数(当然,这假定对于给定CodeSource
还不存在 ProtectionDomain
。如果用一个现有的 CodeSource
装载类,那么就会重复使用它已经建立的 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. 典型调用堆栈的屏幕快照
上面堆栈中的每个类属于一个 ProtectionDomain
,它由其 CodeSource
惟一地标识。一般来说,这样遍历的一组 ProtectionDomain
将包含<=n个元素。(您可能还记得,一组中的每一个 ProtectionDomain
都有相关的一组权限 -- P i
.)像图 1 显示的这样一个调用堆栈快照将编写为(codified)为一个 AccessControlContext
并由 AccessController
对象提供的本机方法调用返回。
访问检查内幕
最后得到的一组适用权限的算法是要计算所有权限的交集。换句话说,某一权限,只有与这个特定瞬间、这个线程的执行堆栈上出现的所有类C i
相应的 ProtectionDomain
相 关联时,这个权限才适用于给定的执行线程。
这种算法的正确性是很显然的。通过计算与调用堆栈上所有 ProtectionDomain
s 相关联的权限集的交集,它保证了不会因为两个类中间的一个(系统/应用程序)调用另一个,而使系统类(通常与更大的 -- 如果不是全部的 -- 一组权限相关联)“泄露”权限给应用程序类(通常与更少的一组权限相关联)。基本上,属于能力更低的域的类不能通过调用属于能力更高的域的类而变得更强大,而属于能力更高的域中的类会在调用能力更低的类时损失其能力。有关这种算法的形式证明请参阅 参考资料。
访问控制方法
确定权限集的交集的算法是在 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()
的方法,在调用堆栈中的当前帧特别做了如上所述的标记时,由运行时执行这个方法。因此,任何时候如果有一些代码,希望在执行时让它的权限 临时性地授予给调用堆栈帧前面的代码时,必须将代码包装为 PrivilegedAction
的形式并用这个对象作为输入调用 AccessController
的 doPrivileged()
方法。
调用堆栈优化
在 图 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
执行。
在这种情况下,权限检查的算法(假定在过程某处,在对 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 最初是作为 JDK 的一个扩展引入的,在版本 1.4 时成为了核心 JDK 的一部分。既然 JAAS 的目的是为了以每位用户为基础控制任何一段代码所能做的事情,因此需要首先能够准确和惟一地标识用户,换句话说,必须能够对他们进行认证。虽然在这里我不会在 JAAS 的“认证”方面花很多时间(有关这个主题的更多参考请参阅 参考资料),但是我将重点介绍它的一个核心组件: Subject
类。
就像以前一直说的,JAAS 是一种用以用户为中心进行授权的方式。在 JAAS 下,相关的问题不再是(像在 Java 2 平台安全体系结构中那样)“哪些是这段代码可以做的?”,而变为“这个认证用户的访问权限是什么?”因此,在本文的其余部分我将着重介绍 JAAS 中 Subject
类的作用,并深入讨论基于 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
的一个实例(它的 run()
方法应当包含需要访问受保护的资源的业务逻辑)。基本思路是应用程序应当首先认证用户,对认证的用户建立了 Subject
后,这个用户可能希望执行的每一个操作都包装为 PrivilegedAction
、并由应用程序作为 Subject
(就像方法自己的名字所表明的 -- doAs()
!)执行。这两个方法之间有细微但是重要的区别,我们将在稍后介绍。
为了能够将操作作为 Subject
执行,必须在调用堆 栈中将 Subject
引入(或者注入)
ProtectionDomain
。这是在一个名为 DomainCombiner
的专用接口的帮助下实现的,我将在开始 doAs()
和 doAsPrivileged()
方法的内幕之前介绍这个接口。
DomainCombiner
如前所述,对于一个 AccessControlContext
(一个调用堆栈),在 JAAS 中将 Subject
注入堆栈中的 ProtectionDomain
是通过实现DomainCombiner
接口(一个特定的实现是 SubjectDomainCombiner
)所处理的。
注入是在将 SubjectDomainCombiner
作为构造函数参数传递以构建 AccessControlContext
时执行的。(作为参数传递给 doAs
调用的Subject
被封装到 SubjectDomainCombiner
对象中,这种封装是在创建后者时,将 Subject
作为构造函数参数传递而完成的。)不过,真正的工作是在 SubjectDomainCombiner
的 combine()
方法中完成的。您将在稍后看到在这个方法中所发生的过程。
Subject.doAs() 方法
应用程序可能期待在认证用户之后调用 Subject.doAs()
方法(即,当 Subject
对用户是可用的时)。在内部,这个调用会产生下列活动:
- 通过调用
AccessController
的getContext()
方法获得当前执行线程的AccessControlContext
。注意,这个调用堆栈当然将会按前面描述的过程优化。 - 创建封装了认证的
Subject
的SubjectDomainCombiner
is。 - 用第 1 步的
AccessControlContext
和第 2 步的SubjectDomainCombiner
创建AccessControlContext
对象。 - 调用
AccessController
的doPrivileged()
方法,将第 2 步创建的AccessControlContext
的PrivilegedAction
实例(下面称为“ privileged
AccessControlContext
”) 作为参数传递给它。 运行时在内部保存 privileged
AccessControlContext
并执行PrivilegedAction
对象的run()
方法。如前所述,在要访问受保护的资源时,需要调用AccessController
类的checkPermission()
方法。在内部,这个调用让
AccessController
寻求当前调用堆栈(即AccessControlContext
)。运行时将返回包含第 4 步介绍的 privilegedAccessControlContext
的AccessControlContext
。如前所述,在检查
AccessControlContext
的帧的ProtectionDomains
是否允许所要求的权限之前,必须优化它。作为这个优化过程的一部分,要求封装在 privilegedAccessControlContext
中的SubjectDomainCombiner
结合当前在调用堆栈上的ProtectionDomains
和在 privilegedAccessControlContext
中出现的ProtectionDomains
。结合过程如下:- 首先,优化 privileged
AccessControlContext
的ProtectionDomain
以删除所有 系统和重复的域。 - 然后,优化当前调用堆栈上的
ProtectionDomain
以删除系统域以及已经出现在 privilegedAccessControlContext
中的域。这时,得到的两组ProtectionDomain
就都没有系统域并且只包含不相同的域。 - 对于从第 b 步得到的每一个 优化的
ProtectionDomain
,创建一个新的ProtectionDomain
,它复制了原来的属性如CodeSource
和Permission
,而且还包含一组与在这个SubjectDomainCombiner
中包含的Subject
相关的Principal
。 - 将优化的
ProtectionDomain
(从第 a 步得到的)附加到新创建的ProtectionDomain
上(从第 c 步得到的)。用这些结合的ProtectionDomain
和SubjectDomainCombiner
创建一个新的AccessControllerContext
并返回它。
- 首先,优化 privileged
现在有了一个优化的
AccessControlContext
(其中这个Subject
的一组Principal
与当前调用堆栈中的每一个ProtectionDomain
相关联),可以安全地调用它的checkPermission()
方法。对
checkPermission()
方法的调用使得运行时在如前所述的循环中遍历包含在这个AccessControlContext
中的一组ProtectionDomain
,并检查每一个ProtectionDomain
是否隐含所要求的Permission
。这里值得注意的一个事实是检查的一组ProtectionDomain
将包括当前调用堆栈的ProtectionDomain
(已经与在认证的Subject
中包含的Principal
相关联)和 privilegedAccessControlContext
中的ProtectionDomain
(在调用doAs()
方法之前的调用堆栈),它还没有与包含在认证的Subject
中的Principal
相关联。所请求的Permission
必须由所有这些ProtectionDomain
隐含。
调用
Subject.doAs()
方法的另一个效果是:可以通过 PrivilegedAction
的 run()
方法达到的任何代码都可以使用认证用户的身份(即Subject
)。得到 Subject
的方法如下:
通过调用
AccessController
的getContext()
方法得到当前AccessControlContext
的句柄。在内部,这个方法以上面第 7 步同样的方式返回一个优化的AccessControlContext
。调用
Subject
类的 staticgetSubject()
方法,将上面获得的AccessControlContext
作为输入参数传递。在内部,在进入下一步之前,它检查调用者是否有getSubject()
方法的javax.security.auth.AuthPermission。
在内部,这个调用提取包含在
AccessControlContext
中的SubjectDomainCombiner
,从提取的SubjectDomainCombiner
中提取出Subject
并返回它。
这样返回的
Subject
表明了认证用户的身份,可以用于登录和/或数据级的授权等。
Subject.doAsPrivileged() 方法
像在 doAs()
方法中看到的那样,在调用 doAs
之前,请求的 Permission
必须由出现在调用堆栈中的 ProtectionDomain
s 所隐含。由于现在已经熟悉的原因,可能不总是希望是这种情况。
正如在讨论
AccessController
类的 doPrivileged()
方法(这个方法以一个 AccessControlContext
为参数用于权限检查)的变种时提到的,PrivilegedAction
可能实际上表示一些服务器代表客户机执行的一些操作(更准确地说是作为客户机,即好像假定服务器具有它代表其执行操作的客户机的身份)。在这种情况下,在调用 doAs
之前调用堆栈的快照将包含服务器的内部代码的 ProtectionDomain
,而让这些 ProtectionDomain
必须隐含一个任意请求的 Permission
显然没有意义。然而,所希望的是以下两种情况之一:
第 I 种情况: 应当用在客户端调用堆栈上的
ProtectionDomains
(当客户机向服务器发送请求的瞬间的快照)检查请求的Permission
(以及与用户身份相关联的服务器端调用堆栈
ProtectionDomain
)。第 II 种情况:应当只用与用户身份相关联的服务器端调用堆栈
ProtectionDomain
进行权限检查。
这个工具是通过
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()
方法时所说,与当前调用堆栈一同返回的还有一个 privilegedAccessControlContext
(包含封装了认证的 Subject
的SubjectDomainCombiner
),所以优化过程可以实际上将一组 Principal
从 Subject
注入到最后一 组 ProtectionDomain
列表中。不过,因为对 AccessController
的 doPrivileged()
方法进行了新的调用,分配了一个新的 privileged 元素,和用这个元素更新的当前执行线程作为最高层的 privileged 元素。因为没有向 doPrivileged
调用传递 AccessControlContext
,所以这个 privileged 元素没有任何 privileged AccessControlContext
与之相关联,这与前面提到的情况不一样。对 getContext
的调用返回直到这个最高 privileged 元素的调用堆栈,因此,有关认证的 Subject
信息在这个执行期间是不可用的。
当然,一旦 inner
PrivilegedAction
执行完,这个 privileged 元素就弹出堆栈,而对 getContext
的所有调用都会再返回包含 privilegedAccessControlContext
的 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 授权模型中的一个矛盾之处,并描述了一种解决它的实用方法,并提供了它的现实世界实现的一个例子。
转自:http://www.ibm.com/developerworks/cn/java/j-javaauth/
微信公众号: 架构师日常笔记 欢迎关注!