JAAS 授权体系结构
Published by admin on 02月 5, 2009
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的SubjectDomainCombineris。 - 用第 1 步的
AccessControlContext和第 2 步的SubjectDomainCombiner创建AccessControlContext对象。 - 调用
AccessController的doPrivileged()方法,将第 2 步创建的AccessControlContext的PrivilegedAction实例(下面称为“ privilegedAccessControlContext”) 作为参数传递给它。 - 运行时在内部保存 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() 方法时所说,与当前调用堆栈一同返回的还有一个 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() 方法时,恢复原来的凭据并与执行线程相关联。

Add A Comment