“Java 认证和授权服务”(Java Authentication and Authorization Service,JAAS)是对 Java 2 SDK 的扩展。在 JAAS 下,可以给予用户或服务特定的许可权来执行 Java 类中的代码。在本文中,软件工程师 Carlos Fonseca 向您展示如何为企业扩展 JAAS 框架。向 JAAS 框架添加类实例级授权和特定关系使您能够构建更动态、更灵活并且伸缩性更好的企业应用程序。请单击文章顶部或底部的 讨论,在 讨论论坛与作者和其他读者分享您对本文的想法。
大多数 Java 应用程序都需要某种类实例级的访问控制。例如,基于 Web 的、自我服务的拍卖应用程序的规范可能有下列要求:
任何已注册(经过认证)的用户都可以创建一个拍卖,但只有创建拍卖的用户才可以修改这个拍卖。
这意味着任何用户都可以执行被编写用来创建 Auction
类实例的代码,但只有拥有该实例的用户可以执行用来修改它的代码。通常情况下,创建 Auction
实例的用户就是所有者。这被称为 类实例所有者关系(class instance owner relationship)。
该应用程序的另一个要求可能是:
任何用户都可以为拍卖创建一个投标,拍卖的所有者可以接受或拒绝任何投标。
再一次,任何用户都可以执行被编写用来创建 Bid
类实例的代码,但只有拥有该实例的用户会被授予修改该实例的许可权。而且, Auction
类实例的所有者必须能够修改相关的 Bid
类实例中的接受标志。这意味着在 Auction
实例和相应的 Bid
实例之间有一种被称为 特定关系(special relationship)的关系。
不幸的是,“Java 认证和授权服务”(JAAS)― 它是 Java 2 平台的一部分 ― 没有考虑到类实例级访问控制或者特定关系。在本文中,我们将扩展 JAAS 框架使其同时包含这两者。推动这种扩展的动力是允许我们将访问控制分离到一个通用的框架,该框架使用基于所有权和特定关系的策略。然后管理员可以在应用程序的生命周期内更改这些策略。
在深入到扩展 JAAS 框架之前,我们将重温一下 Java 2 平台的访问控制机制。我们将讨论策略文件和许可权的使用,并讨论 SecurityManager
和 AccessController
之间的关系。
Java 2 平台中的访问控制
在 Java 2 平台中,所有的代码,不管它是本地代码还是远程代码,都可以由策略来控制。 策略(policy)由不同位置上的代码的一组许可权定义,或者由不同的签发者定义、或者由这两者定义。 许可权允许对资源进行访问;它通过名称来定义,并且可能与某些操作关联在一起。
抽象类 java.security.Policy
被用于表示应用程序的安全性策略。缺省的实现由 sun.security.provider.PolicyFile
提供,在 sun.security.provider.PolicyFile
中,策略被定义在一个文件中。清单 1 是一个典型策略文件示例:
清单 1. 一个典型的策略文件
// Grant these permissions to code loaded from a sample.jar file
// in the C drive and if it is signed by XYZ
grant codebase "file:/C:/sample.jar", signedby "XYZ" {
// Allow socket actions to any host using port 8080
permission java.net.SocketPermission "*:8080", "accept, connect,
listen, resolve";
// Allows file access (read, write, execute, delete) in
// the user's home directory.
Permission java.io.FilePermission "${user.home}/-", "read, write,
execute, delete";
};
|
SecurityManager 对 AccessController
在标准 JDK 分发版中,控制代码源访问的机制缺省情况下是关闭的。在 Java 2 平台以前,对代码源的访问都是由 SecurityManager
类管理的。 SecurityManager
是由 java.security.manager
系统属性启动的,如下所示:
java -Djava.security.manager
|
在 Java 2 平台中,可以将一个应用程序设置为使用 java.lang.SecurityManager
类或者 java.security.AccessController
类管理敏感的操作。 AccessController
在 Java 2 平台中是新出现的。为便于向后兼容, SecurityManager
类仍然存在,但把自己的决定提交 AccessController
类裁决。 SecurityManager
和 AccessController
都使用应用程序的策略文件确定是否允许一个被请求的操作。清单 2 显示了 AccessController
如何处理 SocketPermission
请求:
清单 2. 保护敏感操作
Public void someMethod() {
Permission permission =
new java.net.SocketPermission("localhost:8080", "connect");
AccessController.checkPermission(permission);
// Sensitive code starts here
Socket s = new Socket("localhost", 8080);
}
|
在这个示例中,我们看到 AccessController
检查应用程序的当前策略实现。如果策略文件中定义的任何许可权暗示了被请求的许可权,该方法将只简单地返回;否则抛出一个 AccessControlException
异常。在这个示例中,检查实际上是多余的,因为缺省套接字实现的构造函数也执行相同的检查。
在下一部分,我们将更仔细地看一下 AccessController
如何与 java.security.Policy
实现共同合作安全地处理应用程序请求。
运行中的 AccessController
AccessController
类典型的 checkPermission(Permission p)
方法调用可能会导致下面的一系列操作:
-
AccessController
调用 java.security.Policy
类实现的 getPermissions(CodeSource codeSource)
方法。
-
getPermissions(CodeSource codeSource)
方法返回一个 PermissionCollection
类实例,这个类实例代表一个相同类型许可权的集合。
-
AccessController
调用 PermissionCollection
类的 implies(Permission p)
方法。
- 接下来,
PermissionCollection
调用集合中包含的单个 Permission
对象的 implies(Permission p)
方法。如果集合中的当前许可权对象暗示指定的许可权,则这些方法返回 true
,否则返回 false
。
现在,让我们更详细地看一下这个访问控制序列中的重要元素。
PermissionCollection 类
大多数许可权类类型都有一个相应的 PermissionCollection
类。这样一个集合的实例可以通过调用 Permission
子类实现定义的 newPermissionCollection()
方法来创建。 java.security.Policy
类实现的 getPermissions()
方法也可以返回 Permissions
类实例 ― PermissionCollection
的一个子类。这个类代表由 PermissionCollection
组织的不同类型许可权对象的一个集合。 Permissions
类的 implies(Permission p)
方法可以调用单个 PermissionCollection
类的 implies(Permission p)
方法。
CodeSource 和 ProtectionDomain 类
许可权组合与 CodeSource
(被用于验证签码(signed code)的代码位置和证书)被封装在 ProtectionDomain
类中。有相同许可权和相同 CodeSource
的类实例被放在相同的域中。带有相同许可权,但不同 CodeSource
的类被放在不同的域中。一个类只可属于一个 ProtectionDomain
。要为对象获取 ProtectionDomain
,请使用 java.lang.Class
类中定义的 getProtectionDomain()
方法。
许可权
赋予 CodeSource
许可权并不一定意味着允许所暗示的操作。要使操作成功完成,调用栈中的每个类必须有必需的许可权。换句话说,如果您将 java.io.FilePermission
赋给类 B,而类 B 是由类 A 来调用,那么类 A 必须也有相同的许可权或者暗示 java.io.FilePermission
的许可权。
在另一方面,调用类可能需要临时许可权来完成另一个拥有那些许可权的类中的操作。例如,当从另一个位置加载的类访问本地文件系统时,我们可能不信任它。但是,本地加载的类被授予对某个目录的读许可权。这些类可以实现 PrivilegedAction
接口来给予调用类许可权以便完成指定的操作。调用栈的检查在遇到 PrivilegedAction
实例时停止,有效地将执行指定操作所必需的许可权授予所有的后继类调用。
使用 JAAS
顾名思义,JAAS 由两个主要组件组成:认证和授权。我们主要关注扩展 JAAS 的授权组件,但开始我们先简要概述一下 JAAS 认证,紧接着看一下一个简单的 JAAS 授权操作。
JAAS 中的用户认证
JAAS 通过添加基于 subject 的策略加强了 Java 2 中定义的访问控制安全性模型。许可权的授予不仅基于 CodeSource
,还基于执行代码的用户。显然,要使这个模型生效,每个用户都必须经过认证。
JAAS 的认证机制建立在一组可插登录模块的基础上。JAAS 分发版包含几个 LoginModule
实现。 LoginModules
可以用于提示用户输入用户标识和密码。 LoginContext
类使用一个配置文件来确定使用哪个 LoginModule
对用户进行认证。这个配置可以通过系统属性 java.security.auth.login.config
指定。一个示例配置是:
java -Djava.security.auth.login.config=login.conf
|
下面是一个登录配置文件的样子:
Example {
com.ibm.resource.security.auth.LoginModuleExample required
debug=true userFile="users.xml" groupFile="groups.xml";
};
|
认识您的主体
Subject
类被用于封装一个被认证实体(比如用户)的凭证。一个 Subject
可能拥有一个被称为 主体(principal)的身份分组。例如,如果 Subject
是一个用户,用户的名字和相关的社会保险号可能是 Subject
的某些身份或主体。主体是与身份名关联在一起的。
Principal
实现类及其名称都是在 JAAS 策略文件中指定的。缺省的 JAAS 实现使用的策略文件与 Java 2 实现的策略文件相似 ― 除了每个授权语句必须与至少一个主体关联在一起。 javax.security.auth.Policy
抽象类被用于表示 JAAS 安全性策略。它的缺省实现由 com.sun.security.auth.PolicyFile
提供,在 com.sun.security.auth.PolicyFile
中策略定义在一个文件中。清单 3 是 JAAS 策略文件的一个示例:
清单 3. 示例 JAAS 策略文件
// Example grant entry
grant codeBase "file:/C:/sample.jar", signedby "XYZ",
principal com.ibm.resource.security.auth.PrincipalExample "admin" {
// Allow socket actions to any host using port 8080
permission java.net.SocketPermission
"*:8080", "accept, connect, listen, resolve";
// Allows file access (read, write, execute, delete) in
// the user's home directory.
Permission java.io.FilePermission
"${user.home}/-", "read, write, execute, delete";
};
|
这个示例与清单 1 中所示的标准 Java 2 策略文件相似。实际上,唯一的不同是主体语句,该语句声明只有拥有指定主体和主体名字的 subject(用户)被授予指定的许可权。
再一次,使用系统属性 java.security.auth.policy
指出 JAAS 策略文件驻留在何处,如下所示:
java -Djava.security.auth.policy=policy.jaas
|
Subject
类包含几个方法来作为特殊 subject 执行工作;这些方法如下所示:
public static Object
doAs(Subject subject, java.security.PrivilegedAction action)
public static Object
doAs(Subject subject, java.security.PrivilegedAction action)
throws java.security.PrivilegedActionException
|
注意,用来保护敏感代码的方法与“Java 2 代码源访问控制”(Java 2 CodeSource Access Control)概述中描述的方法相同。请参阅 参考资料部分以了解更多关于 JAAS 中代码源访问控制和认证的信息。
JAAS 中的授权
清单 4 显示一个授权请求的结果,该请求使用清单 3 中显示的 JAAS 策略文件。假设已经安装了 SecurityManager
,并且 loginContext
已经认证了一个带有名为“admin”的 com.ibm.resource.security.auth.PrincipalExample
主体的 Subject
。
清单 4. 一个简单的授权请求
public class JaasExample {
public static void main(String[] args) {
...
// where authenticatedUser is a Subject with
// a PrincipalExample named admin.
Subject.doAs(authenticatedUser, new JaasExampleAction());
...
}
}
public class JaasExampleAction implements PrivilegedAction {
public Object run() {
FileWriter fw = new FileWriter("hi.txt");
fw.write("Hello, World!");
fw.close();
}
}
|
这里,敏感代码被封装在 JaasExampleAction
类中。还要注意,调用类不要求为 JaasExampleAction
类代码源授予许可权,因为它实现了一个 PrivilegedAction
。
扩展 JAAS
大多数应用程序都有定制逻辑,它授权用户不仅仅在类上执行操作,而且还在该类的实例上执行操作。这种授权通常建立在用户和实例之间的关系上。这是 JAAS 的一个小缺点。然而,幸运的是,这样设计 JAAS 使得 JAAS 可以扩展。只要做一点工作,我们将可以扩展 JAAS,使其包含一个通用的、类实例级的授权框架。
在文章开头处我已经说明了,抽象类 javax.security.auth.Policy
被用于代表 JAAS 安全性策略。它的缺省实现是由 com.sun.security.auth.PolicyFile
类提供。 PolicyFile
类从 JAAS 格式的文件(象清单 3 中显示的那个一样)中读取策略。
我们需要向这个文件添加一个东西为类实例级授权扩展策略定义:一个与许可权语句相关的可选关系参数。
缺省 JAAS 许可权语句的格式如下:
permission <permission implementation class> [name], [actions];
|
我们在这个许可权语句的末尾添加一个可选的关系参数来完成策略定义。下面是新许可权语句的格式:
permission <permission implementation class>
[name], [actions], [relationship];
|
在为类实例级授权扩展 JAAS 时要注意的最重要的一点是:许可权实现类必须有一个带三个参数的构造函数。第一个参数是名称参数,第二个是行为参数,最后一个是关系参数。
解析新文件格式
既然文件格式已经改变,就需要一个新的 javax.security.auth.Policy
子类来解析文件。
为简单起见,我们的示例使用了一个新的 javax.security.auth.Policy
子类 com.ibm.resource.security.auth.XMLPolicyFile
,来从 XML 文件读取策略。在实际的企业应用程序中,关系数据库更适合执行这个任务。
使用 XMLPolicyFile
类代替缺省的 JAAS 访问控制策略实现的最容易的方法是向 java.security
属性文件添加 auth.policy.provider=com.ibm.resource.security.auth.XMLPolicyFile
条目。 java.security
属性文件位于 Java 2 平台运行时的 lib/security 目录下。清单 5 是与 XMLPolicyFile
类一起使用的样本 XML 策略文件:
清单 5. 一个 XML 策略文件
<?xml version="1.0"?>
<policy>
<grant codebase="file:/D:/sample_actions.jar">
<principal classname=
"com.ibm.resource.security.auth.PrincipalExample" name="users">
<permission classname=
"com.ibm.resource.security.auth.ResourcePermission"
name="com.ibm.security.sample.Auction"
actions="create" />
<permission classname=
"com.ibm.resource.security.auth.ResourcePermission"
name="com.ibm.security.sample.Auction"
actions="read" />
<permission classname=
"com.ibm.resource.security.auth.ResourcePermission"
name="com.ibm.security.sample.Auction"
actions="write"
relationship="owner" />
<permission classname=
"com.ibm.resource.security.auth.ResourcePermission"
name="com.ibm.security.sample.Bid"
actions="create" />
<permission classname=
"com.ibm.resource.security.auth.ResourcePermission"
name="com.ibm.security.sample.Bid"
actions="read" />
<permission classname=
"com.ibm.resource.security.auth.ResourcePermission"
name="com.ibm.security.sample.Bid"
actions="write"
relationship="owner" />
<permission classname=
"com.ibm.resource.security.auth.ResourcePermission"
name="com.ibm.security.sample.Bid"
actions="accept"
relationship="actionOwner" />
</principal>
</grant>
</policy>
|
在这个示例策略文件中,任何与名为 PrincipalExample
的用户有关的用户( Subject
)都可以创建并读取一个 Auction.class
实例。但是,只有创建该实例的用户才可以更新(写)它。这是第三个 permission 元素定义的,该元素包含值为 owner 的 relationship 属性。 Bid.class
实例也是一样,除了相应 Auction.class
实例的所有者可以更改投标接受标志。
Resource 接口
要求类实例级访问控制的类必须实现 Resource
接口。该接口的 getOwner()
方法返回类实例的所有者。 fulfills(Subject subject, String relationship)
方法被用于处理特定关系。另外,这些类使用 com.ibm.resource.security.auth.ResourcePermission
类保护敏感代码。例如, Auction
类拥有下列构造函数:
public Auction() {
Permission permission =
new ResourcePermission("com.ibm.security.sample.Auction", "create");
AccessController.checkPermission(permission);
}
|
所有者关系
ResourcePermission
类的 implies(Permission p)
方法是这个框架的关键。 implies()
方法就等同性比较名称和行为属性。如果定义了一个关系,那么必须把受保护的类实例( Resource
)传递到 ResourcePermission
构造函数中。 ResourcePermission
类理解所有者关系。它将类实例的所有者与执行代码的 subject(用户)进行比较。特定关系被委托给受保护类的 fulfills()
方法。
例如,在清单 5 中所示的 XML 策略文件中,只有 Auction
类实例的所有者可以更新(写)文件。该类的 setter 方法使用清单 6 中显示的保护代码:
清单 6. 运行中的 implies(Permission) 方法
public void setName(String newName) {
Permission permission =
new ResourcePermission("com.ibm.security.sample.Auction", "write", this);
AccessController.checkPermission(permission);
// sensitive code
this.name = newName;
}
|
被传递到 ResourcePermission
构造函数中的 this
引用代表 Auction
类实现的 Resource
接口。由于策略文件中列出的关系是 owner,所以 ResourcePermission
类使用这个引用检查当前 Subject
(用户)是否拥有与实例所有者相匹配的主体。如果指定了另一个关系,那么 ResourcePermission
类调用 Auction
类的 fulfills(Subject subject, String relationship)
方法。由 Resource
实现类提供 fulfills()
方法中的逻辑。
XML 策略文件中列出的 Bid
类拥有清单 7 中所示的方法(假设 Bid
类实例有一个对相应 Auction
类实例的引用 ― auction)。
清单 7. 处理特定关系
public void setAccepted(boolean flag) {
Permission permission =
new ResourcePermission("com.ibm.security.sample.Auction", "accept", this);
AccessController.checkPermission(permission);
// sensitive code
this.accepted = flag;
}
public boolean fulfills(Subject user, String relationship) {
if( relationship.equalsIgnoreCase("auctionOwner") ) {
String auctionOwner = auction.getOwner();
Iterator principalIterator = user.getPrincipals().iterator();
while(principalIterator.hasNext()) {
Principal principal = (Principal) principalIterator.next();
if( principal.getName().equals(auctionOwner) )
return true;
}
}
return false;
}
|
传递到 fulfills()
方法中的关系字符串是策略文件中列出的关系。在这个案例中,我们使用了“ auctionOwner
”字符串。
缺省情况下, XMLPolicyFile
类在当前工作目录中查找名为 ResourcePolicy.xml
的文件。系统属性 com.ibm.resource.security.auth.policy
可以用于指定另一个不同的文件名和位置。
一个可运行的示例
综合这些信息,我们将运行一个简单的命令行示例。该示例程序包含三个 jar 文件:
- resourceSecurity.jar
- example.jar
- exampleActions.jar
resourceSecurity.jar 文件包含允许实例级访问控制的 JAAS 扩展框架。它还包含一个 LoginModuleExample
类,这个类从 XML 文件读取用户认证信息。用户标识和密码存储在 users.xml 文件中。用户组存储在 groups.xml 文件中。关于 LoginModuleExample
的更多信息,请参阅 参考资料部分。
该示例包含四个附加的文件:
- login.conf
- policy
- resourcePolicy.xml
- run.bat
在试图运行这个示例程序之前,请确保更新了 run.bat、policy 和 resourcePolicy.xml 文件中的路径。缺省情况下,所有的密码都是“passw0rd”。
示例如何工作
该示例程序提示输入用户标识和密码。它用 users.xml 文件中的条目核对所提供的用户标识和密码。在认证了用户之后,程序设法创建一个 UserProfile
类实例,修改它并从中读取。缺省情况下, UserProfile
类的所有者是 Jane(jane)。当 Jane 登录时,三个操作全部成功。当 John(john)登录时,只有创建操作成功。当 Jane 的经理 Lou(lou)登录时,只有第一个和最后一个操作成功。当系统管理员(admin)登录时,操作全部成功。当然,只有当提供的 ResourcePolicy.xml 文件未被修改时,上述这些才都是真的。
示例安装
下面的安装指导假设您正在使用 JDK 1.3 并且已经把文件解压缩到 d:\JaasExample 目录。通过将文件解压缩到这个目录,您可以省去一些工作;否则您就必须使用正确的路径名修改 policy 和 ResourceSecurity.xml 策略文件。
下面是运行该示例需要做的工作:
- 下载这个示例的 源文件。
- 把 jaas.jar 和 jaasmod.jar 复制到 JDK jre\lib\ext 目录(即 D:\JDK1.3\jre\lib\ext)。
- 向位于 JDK 的 jre\lib\security 目录(即 D:\JDK1.3\jre\lib\security)中的 java.security 文件的末尾添加下面的字符串:
auth.policy.provider=com.ibm.resource.security.auth.XMLPolicyFile
。
- 执行 run.bat 文件。
结束语
类实例级授权把访问控制分离到一个通用框架(该框架使用基于所有权和特定关系的策略)中。然后管理员可以在应用程序的生命周期内更改这些策略。用这种方法扩展 JAAS 减少了您或另一个程序员必须在应用程序生命周期内业务规则发生更改时重写代码的可能性。
通过将关系字符串抽象为类可以进一步扩展特定关系这个概念。不调用 Resource
实现类的 fulfills(Subject user, String relationship)
方法,而只要调用 Relationship
实现类中定义的新 fulfills(Subject user, Resource resource)
方法。这样就会允许许多 Resource
实现类使用相同的关系逻辑。