`

Java 安全性 – 认证与授权

阅读更多

原文地址:http://www.ibm.com/developerworks/cn/views/java/tutorials.jsp?cv_doc_id=84887

关于作者

Brad Rubin 是 Brad Rubin & Associates Inc. 的负责人,该公司是一家专门从事无线网络与 Java 应用程序安全性和教育的计算机安全性咨询公司。Brad 在 IBM(位于明尼苏达州的 Rochester)工作了 14 年,从 AS/400 的第一个发行版开始, 就从事它的硬件和软件所有方面的开发。他是促使 IBM 转向支持 Java 平台的关键人物,并且是 IBM 最大的 Java 应用程序(称为 SanFrancisco (现在是 WebSphere 的一部分) 的商业应用程序框架产品)的首席架构设计师。他还是 Imation Corp. 数据存储部门(Data Storage Division)的首席技术官, 还是其研发组织的领导。

Brad 拥有计算机和电子工程学位,以及威斯康星大学麦迪逊分校计算机科学博士头衔。 他目前在明尼苏达大学教授电子和计算机工程高级设计(Senior Design)课程,并将于 2002 年秋季开发和教授该大学的计算机安全性(Computer Security)课程。可以通过 BradRubin@BradRubin.com 与 Brad 联系。

工具、代码样本和安装需求

JAAS 本来是 Java 2 平台标准版的扩展。然而,最近已将它添加到版本 1.4 中。要完成本教程,需要下列各项:

  • JDK 1.4,标准版
  • 教程源代码和类,以便您可以跟上我们的进度并理解示例。
  • 支持 Java 1.4 插件的浏览器。

可以使用 JDK 1.3.x,但您必须自行安装 JCE 和 JSSE。

认证与授权

认证 是用户或计算设备用来验证身份的过程。授权 是根据请求用户的身份允许访问和操作一段敏感软件的过程。 这两个概念密不可分。没有授权, 就无需知道用户的身份。没能认证,就不可能区分可信和不可信用户, 更不可能安全地授权访问许多系统部分。

不一定要标识或认证个别实体;在某些情况下,可以通过分组,对给定组中的所有实体授予某种权限来进行认证。 在某些情况下,个别认证是系统安全性必不可少的环节。

认证与授权的另一个有趣方面是,一个实体在系统中可以有几个角色。 例如,用户可以同时是公司职工(表示他需要对公司的电子邮件有访问权)和该公司的会计师(表示他需要对公司财务系统有访问权)。

认证元素

认证基于以下一个或多个元素:

  • 您知道什么。 该类别包括个别人知道而其它人一般不知道的信息。示例包括 PIN、密码和个人信息(如母亲的婚前姓)。
  • 您有些什么。 该类别包括使个人能够访问资源的物理项。 示例包括 ATM 卡、Secure ID 令牌和信用卡。
  • 您是谁。 该类别包括如指纹、视网膜剖面和面部照片等生物测定信息。

通常,对于授权只使用一种类别是不够的。例如,ATM 卡通常与 PIN 结合在一起使用。 即使物理卡丢失,用户和系统也能够安然无恙,因为小偷还必须知道 PIN 才能访问任何资源。

授权元素

有两种控制访问敏感代码的基本方法:

  • 声明性 授权可以由系统管理员执行,他配置系统的访问权(即,声明谁可以访问系统中的哪些应用程序)。 通过声明性授权,可以添加、更改或取消用户访问特权,而不影响底层应用程序代码。
  • 程序性 授权使用 Java 应用程序代码来做授权决定。 当授权决定需要更复杂的逻辑和决定(超出了声明性授权的能力范围)时,程序性授权是必需的。 因为程序性授权被构建到应用程序代码中,所以更改程序性授权时要求重写应用程序的部分代码。

您将在本教程中学习声明性和程序性授权技术。

保护用户和代码

根据用户在代码中的可信度,Java 平台允许对计算资源(如磁盘文件和网络连接)进行细颗粒度的访问控制。Java 平台的大多数基本安全性特性都是为保护用户免受潜在的恶意代码破坏而设计的。例如, 第三方证书支持的数字签名代码确保代码来源的身份。 根据用户对代码来源的了解,他可以选择授予或拒绝对该代码的执行权。同样,用户可以根据给定代码来源的下载 URL 授予或拒绝访问权。

基于 Java 的系统上的访问控制是通过策略文件实现的,该文件包含的语句如下:

grant signedBy "Brad", codeBase "http://www.bradrubin.com" {

       permission java.io.FilePermission "/tmp/abc", "read";

};

该语句允许由“Brad”签署并从 http://www.bradrubin.com 装入的代码读取 /tmp/abc 目录。

其它 Java 平台特性(如缺少指针)进一步保护用户免受潜在的恶意代码破坏。JAAS 的认证和授权服务一起工作,提供了补充功能:它们防止敏感的 Java 应用程序代码遭到潜在的恶意用户破坏。

可插入认证模块

JAAS 实现“可插入认证模块(Pluggable Authentication Module(PAM))”框架的 Java 版本。Sun Microsystems 为其 Solaris 操作系统创建了 PAM;通过 JAAS,现在可以以独立于平台的形式使用 PAM。

PAM 的主要用途是允许应用程序开发人员在开发时写入标准认证接口, 并将使用哪些认证技术(以及如何使用它们)的决策留给系统管理员。 认证技术是在登录模块中实现的, 这些登录模块是在编写了应用程序之后部署的,并且在称为登录配置文件 (本教程中名为 login.config)的文本文件中指定。login.config 文件不仅可以指定要调用哪些模块,而且还可以指定总体认证成功的条件。

PAM 使新的认证技术或技巧能更方便地添加到现有应用程序中。 同样,可以通过更新 login.config 文件来更改认证策略,而不是重写整个应用程序。

JDK 1.4 是与下列 PAM 模块一起提供的。稍后,我们将在本教程中使用其中一个模块,并还要练习编写我们自己的两个模块:

  • com.sun.security.auth.module.NTLoginModule
  • com.sun.security.auth.module.NTSystem
  • com.sun.security.auth.module.JndiLoginModule
  • com.sun.security.auth.module.KeyStoreLoginModule
  • com.sun.security.auth.module.Krb5LoginModule
  • com.sun.security.auth.module.SolarisSystem
  • com.sun.security.auth.module.UnixLoginModule
  • com.sun.security.auth.module.UnixSystem

JAAS 示例和图

在本教程中,我们将逐一研究 JAAS 示例应用程序的代码。为了对总体情况有所了解, 下图显示了所有这些代码是如何组合在一起的。正在运行的示例(主程序 JAASExample)先使用两种技术(即两个登录模块)来认证用户,然后根据认证步骤的结果允许或禁止(或授权)访问两段敏感代码。

下面是 JAASExample 程序的图。下一页将描述操作流。

JAAS

JAAS

JAASExample 操作流

下面是由 JAASExample 图说明的总体认证与授权流的简要描述。以下每个步骤将在本教程的其它地方进行更为详细的描述。

我们从认证的第一步开始,就是要创建登录环境并试图登录。LoginContext 是一个 Java 类,它使用 login.config 文件中的信息来决定要调用哪些登录模块以及将使用什么标准来确定是否成功。 对本示例,有两个登录模块。 第一个登录模块是 AlwaysLoginModule ,它不需要密码,所以它总是成功的(这是不切实际的,但它足以说明 JAAS 是如何工作的)。 该模块用关键字 required 标记,表示它是成功所必需的(它总是成功)。第二个登录模块是PasswordLoginModule ,它需要密码,但该模块的成功与否是可选的,因为它用关键字 optional 标记。这表示即使 PasswordLoginModule 失败,但总体登录仍可成功。

初始化之后,选择的登录模块经历由 LoginContext 控制的两阶段提交过程。 作为该过程的一部分,调用 UsernamePasswordCallbackHandler 以获取个人(用 Subject 对象表示)的用户名和密码。如果认证成功,则 Principal 被添加到 Subject 中。Subject 可以有许多 Principal (在该示例中,是“Brad”和“joeuser”),每个 Principal 都授予用户对系统的不同级别的访问权。这样就完成了认证步骤。

一旦认证完成,通过使用程序认证技术和 doAs 方法,用 Subject 来尝试执行一些敏感的工资单操作代码。JAAS 检查是否授予 Subject 访问权。 如果 Subject 有一个授权访问工资单代码的 Principal , 那么允许继续执行。否则,将拒绝执行。

接下来,我们尝试使用声明性授权技术和 doAsPrivilaged 方法来执行一些敏感的职员信息操作代码。这次,JAAS 部署用户定义的特权(PersonnelPermission )、Java 策略文件(jaas.policy)和 Java 访问控制器(AccessController )用来决定是否可以继续执行。

Subject 和 Principal

Subject 是一种 Java 对象,它表示单个实体,如个人。 一个 Subject 可以有许多个相关身份,每个身份都由一个 Principal 对象表示。那么,比方说一个 Subject 表示要求访问电子邮件系统和财务系统的雇员。 该 Subject 将有两个 Principal , 一个与用于电子邮件访问的雇员的用户标识关联, 另一个与用于财务系统访问的用户标识关联。

Principal 不是持久性的,所以每次用户登录时都必须将它们添加到 SubjectPrincipal 作为成功认证过程的一部分被添加到 Subject 。 同样,如果认证失败,则从 Subject 中除去 Principal 。 不管认证成功与否,当应用程序执行注销时,将除去所有 Principal

除了包含一组 Principal 外,Subject 还可以包含两组凭证:公用和专用。credential 是密码、密钥和令牌等。对公用和专用凭证集的访问是由 Java 特权控制的, 稍后,我们将在本教程中讨论它。对凭证的完整讨论超出了本教程的范围。

Subject 的方法

Subject 对象有几个方法,其中一些方法如下:

  • subject.getPrincipals() 返回一组 Principal 对象。因为结果是 Set ,所以适用操作 remove()add()contains()
  • subject.getPublicCredentials() 返回一组与 Subject 相关的公用可访问凭证。
  • subject.getPrivateCredentials() 返回一组与 Subject 相关的专用可访问凭证。

Principal 接口

Principal 是一个 Java 接口。程序员编写的 PrincipalImpl 对象与 Serializable 接口、名称字符串、返回该字符串的 getName() 方法以及其它支持方法(如 hashCode()toString()equals() )一起实现 Principal 接口。

在登录过程期间,Principal 被添加到 Subject 。正如我们稍后将看到的那样, 声明性授权基于策略文件中的项。进行授权请求时,将系统的授权策略与包含在 Subject 中的 Principal 进行比较。如果 Subject 有一个满足策略文件中安全性需求的 Principal ,则授权;否则拒绝。

PrincipalImpl

这里是我们将在本教程中使用的 PrincipalImpl

import java.io.Serializable;
import java.security.Principal
;

//
// This class defines the principle object, which is just an encapsulated
// String name
public class PrincipalImpl implements Principal, Serializable {

     private String name;

     public PrincipalImpl(String n) {
       name = n;
     }

     public boolean equals(Object obj) {
       if (!(obj instanceof PrincipalImpl)) {
         return false;
       }
       PrincipalImpl pobj = (PrincipalImpl)obj;
       if (name.equals(pobj.getName())) {
         return true;
       }
       return false;
     }

     public String getName() {
       return name;
     }

     public int hashCode() {
       return name.hashCode();
     }

     public String toString() {
       return getName();
     }

}

登录配置

JAAS 允许在以下几个方面有极大的灵活性:Subject 需要的认证过程种类、它们的执行顺序以及在 Subject 被认为是已认证的之前要求的认证成功或失败的组合。

JAAS 使用 login.config 文件来指定每个登录模块的认证项。login.config 文件是在 Java 执行命令行上用特性 -Djava.security.auth.login.config==login.config 指定的。Java 有缺省登录配置文件,所以双等于号(== )替换系统登录配置文件。如果使用一个等于号,login.config 文件将被添加到(而不是替换)系统登录配置文件。因为我们不知道您的系统文件中可能会有什么, 所以我们这样做来确保对于各种各样的教程用户都可以得到可靠的结果。

login.config 文件包含 LoginContext 构造器中引用的文本字符串和登录过程列表。 几个参数用于指定一个给定的登录过程的成功或失败对总体认证过程的影响。 有如下参数:

  • required 表示登录模块必须成功。即使它不成功,还将调用其它登录模块。
  • optional 表示登录模块可以失败,但如果另一个登录模块成功,总体登录仍可以成功。 如果所有登录模块都是可选的,那么要使整个认证成功至少必须有一个模块是成功的。
  • requisite 表示登录模块必须成功,而且如果它失败,将不调用其它登录模块。
  • sufficient 表示如果登录模块成功,则总体登录将成功,同时假设没有其它必需或必不可少的登录模块失败。

示例 login.config 文件

我们将在本教程中使用的 login.config 文件如下:

JAASExample {
      AlwaysLoginModule required;
      PasswordLoginModule optional;
};

正如您看到的那样,AlwaysLoginModule 必须成功,而 PasswordLoginModule 可以成功也可以失败。
这不是一种现实的情形,稍后我们将修改这些参数来查看不同的配置如何更改代码行为。
对于这项登录配置技术,应该认识到它将所有主要决定(如所需的认证类型和认证成功或失败的特定标准)都留到建立部署时决定,这很重要。 成功的登录将导致新的 Subject 添加到 LoginContext , 同时将所有成功认证的 Principal 添加到该 Subject

登录环境

LoginContext 是一种用于设置登录过程的 Java 类,它进行实际的登录,如果登录成功,获取 Subject 。 它有如下四种主要方法:

  • LoginContext("JAASExample", newUsernamePasswoerdCallbackHandler()) 是构造器。 它把 login.config 文件中使用的字符串作为其第一个参数,把执行实际任务的回调处理程序作为其第二个参数。 (接下来,我们将讨论回调处理程序。)
  • login() ,它根据 login.config 文件中指定的规则实际尝试登录。
  • getSubject() ,如果登录总体成功,它返回经认证的 Subject
  • logout() ,它向 LoginContext 注销 Subject

回调处理程序

JAAS 登录使用回调处理程序来获取用户的认证信息。CallbackHandler 是在 LoginContext 对象的构造函数中指定的。在本教程中,回调处理程序使用几个提示来获取用户的用户名和密码信息。 从登录模块调用的处理程序的 handle() 方法将 Callback 数组对象作为其参数。在登录期间,处理程序遍历 Callback 数组。handle() 方法检查 Callback 对象的类型并执行适当的用户操作。Callback 类型如下:

  • NameCallback
  • PasswordCallback
  • TextInputCallback
  • TextOutputCallback
  • LanguageCallback
  • ChoiceCallback
  • ConfirmationCallback

在某些应用程序中,因为 JAAS 将用于与操作系统的认证机制相互操作,所以不需要任何用户交互。 在这种情况下,LoginContext 对象中的 CallbackHandler 参数将是空的。

回调处理程序代码

下面是本教程中使用的 UsernamePasswordCallbackHandler 的代码。它由 AlwaysLoginModule 调用一次(仅一次回调以获取用户标识),由 PasswordLoginModule 调用一次(两次回调以获取用户标识和密码)。

import java.io.*;
import java.security.*;
import javax.security.auth.*;
import javax.security.auth.callback.*;
//
// This class implements a username/password callback handler that gets
// information from the user
public class UsernamePasswordCallbackHandler implements CallbackHandler {
     //
     // The handle method does all the work and iterates through the array
     // of callbacks, examines the type, and takes the appropriate user
     // interaction action.
     public void handle(Callback[] callbacks) throws
         UnsupportedCallbackException, IOException {

       for(int i=0;i<callbacks.length;i++) {
         Callback cb = callbacks[i];
         //
         // Handle username aquisition
         if (cb instanceof NameCallback) {
           NameCallback nameCallback = (NameCallback)cb;
           System.out.print( nameCallback.getPrompt() + "? ");
           System.out.flush();
           String username = new BufferedReader(
               new InputStreamReader(System.in)).readLine();
           nameCallback.setName(username);
           //
           // Handle password aquisition
         } else if (cb instanceof PasswordCallback) {
           PasswordCallback passwordCallback = (PasswordCallback)cb;
           System.out.print( passwordCallback.getPrompt() + "? ");
           System.out.flush();
           String password = new BufferedReader(
               new InputStreamReader(System.in)).readLine();
           passwordCallback.setPassword(password.toCharArray());
           password = null;
           //
           // Other callback types are not handled here
         } else {
           throw new UnsupportedCallbackException(cb, "Unsupported
Callback Type");
         }
       }
     }
}

登录模块

LoginModule 是参与 JAAS 认证过程所需的方法的接口。 因为可能要到执行其它登录过程时才知道特定登录过程是成功还是失败,所以用两阶段提交过程来确定是否成功。 下列方法由 LoginModule 对象实现:

  • initialize( subject, callbackHandler, sharedState, options) 初始化 LoginModule 。(注:对 sharedStateoptions 的讨论超出了本教程的范围。)
  • login() 设置任何必需的回调,调用 CallbackHandler 来处理它们, 并将返回的信息(即用户名和密码)与允许值进行比较。如果匹配,则登录模块成功, 尽管仍可能因为另一个登录模块不成功而异常终止它,这取决于 login.config 文件中的设置。
  • commit() 作为两阶段提交过程的一部分被调用以确定是否成功。 如果根据 login.config 文件中指定的约束,所有登录模块都是成功的, 那么新的 Principal 随同用户名一起创建,并被添加到 Subject 的主体集。
  • abort() ,如果总体登录未成功,则调用它;如果发生异常终止,必须清除内部的 LoginModule 状态。
  • logout() 被调用以除去 Subject 的主体集中的 Principal 并执行其它内部状态清除。

下面两页说明了两个登录模块。第一个是 AlwaysLoginModule ,它始终是成功的。 第二个是 PasswordLoginModule ,仅当用户标识和密码与某些硬编码值匹配时,它才会成功。 虽然两个示例模块都不是合乎实际的实现,但它们共同演示了各种 JAAS 选项的结果。

AlwaysLoginModule

AlwaysLoginModule 认证将始终成功, 所以实际上它仅用于通过 NameCallback 函数获取用户名。 假设其它登录模块都成功,AlwaysLoginModulecommit() 方法将创建一个带用户名的新 PrincipalImpl 对象并将它添加到 SubjectPrincipal 集中。注销将除去 SubjectPrincipal 集 中的 PrincipalImpl

import java.security.*;
import javax.security.auth.*;
import javax.security.auth.spi.*;
import javax.security.auth.callback.*;
import javax.security.auth.login.*;
import java.io.*;
import java.util.*;

// This is a JAAS Login Module that always succeeds.  While not realistic,
// it is designed to illustrate the bare bones structure of a Login Module
// and is used in examples that show the login configuration file
// operation.

public class AlwaysLoginModule implements LoginModule {

     private Subject
 subject;
     private Principal
 principal;
     private CallbackHandler callbackHandler;
     private String username;
     private boolean loginSuccess;
     //
     // Initialize sets up the login module.  sharedState and options are
     // advanced features not used here
     public void initialize(Subject
 sub, CallbackHandler cbh,
       Map sharedState, Map options) {

       subject = sub;
       callbackHandler = cbh;
       loginSuccess = false;
     }
     //
     // The login phase gets the userid from the user
     public boolean login() throws LoginException {
       //
       // Since we need input from a user, we need a callback handler
       if (callbackHandler == null) {
         throw new LoginException( "No CallbackHandler defined");

       }
       Callback[] callbacks = new Callback[1];
       callbacks[0] = new NameCallback("Username");
       //
       // Call the callback handler to get the username
       try {
         System.out.println( "\nAlwaysLoginModule Login" );
         callbackHandler.handle(callbacks);
         username = ((NameCallback)callbacks[0]).getName();
       } catch (IOException ioe) {
         throw new LoginException(ioe.toString());
       } catch (UnsupportedCallbackException uce) {
         throw new LoginException(uce.toString());
       }
       loginSuccess = true;
       System.out.println();
       System.out.println( "Login: AlwaysLoginModule SUCCESS" );
       return true;
     }
     //
     // The commit phase adds the principal if both the overall authentication
     // succeeds (which is why commit was called) as well as this particular
     // login module
     public boolean commit() throws LoginException {
       //
       // Check to see if this login module succeeded (which it always will

       // in this example)
       if (loginSuccess == false) {
         System.out.println( "Commit: AlwaysLoginModule FAIL" );
         return false;
       }
       //
       // If this login module succeeded too, then add the new principal
       // to the subject (if it does not already exist)
       principal = new PrincipalImpl(username);
       if (!(subject.getPrincipals().contains(principal))) {
         subject.getPrincipals().add(principal);
       }
       System.out.println( "Commit: AlwaysLoginModule SUCCESS" );
       return true;
     }
     //
     // The abort phase is called if the overall authentication fails, so
     // we have to clean up the internal state
     public boolean abort() throws LoginException {

       if (loginSuccess == false) {
         System.out.println( "Abort: AlwaysLoginModule FAIL" );
         principal = null;
         return false;
       }
       System.out.println( "Abort: AlwaysLoginModule SUCCESS" );
       logout();

       return true;
     }
     //
     // The logout phase cleans up the state
     public boolean logout() throws LoginException {

       subject.getPrincipals().remove(principal);
       loginSuccess = false;
       principal = null;
       System.out.println( "Logout: AlwaysLoginModule SUCCESS" );
       return true;
      }
}

PasswordLoginModule

PasswordLoginModule 使用 NameCallback 来获取用户名并使用 PasswordCallback 来获取密码。如果用户名是“joeuser”,密码是“joe”, 则该认证将成功。

import java.security.*;
import javax.security.auth.*;
import javax.security.auth.spi.*;
import javax.security.auth.callback.*;
import javax.security.auth.login.*;
import java.io.*;
import java.util.*;
//
// This is a JAAS Login Module that requires both a username and a
// password. The username must equal the hardcoded "joeuser" and
// the password must match the hardcoded "joeuserpw".
public class PasswordLoginModule implements LoginModule {

     private Subject
 subject;
     private Principal
 principal;
     private CallbackHandler callbackHandler;
     private String username;
     private char[] password;
     private boolean loginSuccess;
     //
     // Initialize sets up the login module.  sharedState and options are
     // advanced features not used here
     public void initialize(Subject
 sub, CallbackHandler cbh,
       Map sharedState,Map options) {

       subject = sub;
       callbackHandler = cbh;
       loginSuccess = false;
       username = null;
       clearPassword();
     }
     //
     // The login phase gets the userid and password from the user and
     // compares them to the hardcoded values "joeuser" and "joeuserpw".
     public boolean login() throws LoginException {
       //
       // Since we need input from a user, we need a callback handler
       if (callbackHandler == null) {
          throw new LoginException("No CallbackHandler defined");
       }
       Callback[] callbacks = new Callback[2];
       callbacks[0] = new NameCallback("Username");
       callbacks[1] = new PasswordCallback("Password", false);
       //
       // Call the callback handler to get the username and password
       try {
         System.out.println( "\nPasswordLoginModule Login" );
         callbackHandler.handle(callbacks);
         username = ((NameCallback)callbacks[0]).getName();
         char[] temp = ((PasswordCallback)callbacks[1]).getPassword();
         password = new char[temp.length];
         System.arraycopy(temp, 0, password, 0, temp.length);
         ((PasswordCallback)callbacks[1]).clearPassword();
       } catch (IOException ioe) {
         throw new LoginException(ioe.toString());
       } catch (UnsupportedCallbackException uce) {
         throw new LoginException(uce.toString());
       }
       System.out.println();
       //
       // If username matches, go on to check password
       if ( "joeuser".equals(username)) {
         System.out.println
           ( "Login: PasswordLoginModule Username Matches" );
         if ( password.length == 5 &&
             password[0] == 'j' &&
             password[1] == 'o' &&
             password[2] == 'e' &&
             password[3] == 'p' &&
             password[4] == 'w' ) {
           //
           //If userid and password match, then login is a success
           System.out.println
             ( "Login: PasswordLoginModule Password Matches" );
           loginSuccess = true;
           System.out.println
             ( "Login: PasswordLoginModule SUCCESS" );
           clearPassword();
           return true;
         } else {
           System.out.println
            ( "Login: PasswordLoginModule Password Mismatch" );
         }
       } else {
         System.out.println( "Login: PasswordLoginModule Username Mismatch" );
       }
       //
       // If either mismatch, then this login module fails

       loginSuccess = false;
       username = null;
       clearPassword();
       System.out.println( "Login: PasswordLoginModule FAIL" );
       throw new FailedLoginException();
     }
     //
     // The commit phase adds the principal if both the overall
     // authentication succeeds (which is why commit was called)
     // as well as this particular login module
     public boolean commit() throws LoginException {

       //
       // Check to see if this login module succeeded
       if (loginSuccess == false) {
         System.out.println( "Commit: PasswordLoginModule FAIL" );
         return false;
       }
       // If this login module succeeded too, then add the new principal
       // to the subject (if it does not already exist)
       principal = new PrincipalImpl(username);
       if (!(subject.getPrincipals().contains(principal))) {
         subject.getPrincipals().add(principal);
       }
       username = null;
       System.out.println( "Commit: PasswordLoginModule SUCCESS" );
       return true;
     }
     //
     // The abort phase is called if the overall authentication fails, so
     // we have to cleanup the internal state
     public boolean abort() throws LoginException {

       if (loginSuccess == false) {
         System.out.println( "Abort: PasswordLoginModule FAIL" );
         principal = null;
         username = null;
         return false;
       }
       System.out.println( "Abort: PasswordLoginModule SUCCESS" );
       logout();
       return true;
     }
     //
     // The logout phase cleans up the state
     public boolean logout() throws LoginException {
       subject.getPrincipals().remove(principal);
       loginSuccess = false;
       username = null;
       principal = null;
       System.out.println( "Logout: PasswordLoginModule SUCCESS" );
       return true;
     }
     //
     // Private helper function to clear the password, a good programming
     // practice
     private void clearPassword() {
       if (password == null) {
         return;
       }
       for (int i=0;i<password.length;i++) {
         password[i] = ' ';
       }
       password = null;
     }
}

Java 平台使用访问控制环境(access control context) 的概念来确定当前执行线程的权限。 从概念上讲,可以将它视作与每个执行线程连接的令牌。 在 JAAS 之前,访问控制基于了解当前 Java .class 文件的代码来源或数字签名者的身份。在这种模型下, 访问控制是基于了解代码出自于何处。有了 JAAS,我们将模型转了个向。通过将 Subject 添加到访问控制环境, 我们可以开始根据谁正在执行(或要求执行)一段给定代码来授予或拒绝访问权。

访问控制和权限

因为执行线程可以跨越多个具有不同环境特征的模块,所以 Java 平台实现了最小特权 这一概念。 在属于给定执行线程的整个调用程序栈中,调用栈的成员具有不同特征, 用于确定权限的结果是所有这些特征的交集或最小公分母。例如, 如果一段调用代码有受限权限(可能由于未对它签名,所以它不可信),但它调用一段信任度较高的代码(可能有一个签名), 则降低 被调用代码中的权限来匹配较低的信任度。

将包含在访问控制环境中的权限特征与策略文件中的 Java 权限 grant 语句进行比较,以表明是否允许敏感操作。这是由名为 AccessController 的 Java 实用程序完成的,它的接口用于通过程序检查特权以及将当前的 Subject 与活动的访问控制环境相关联。(较旧的 Java 安全性管理器(Java Security Manager)接口已经过时,所以一定要使用 AccessController 方法。)

将 Subject 绑定到访问控制环境

因为可以在应用程序启动之后认证 Subject , 所以必须有一个将 Subject 动态绑定到访问控制环境的方法,以创建一个包含代码权限(从何处装入它以及谁对它进行签名)和用户权限(Subject )环境。 为此,我们使用方法 Object doAs(Subject subject, PrivilegedAction action) 。 这个 doAs 方法调用特别为授权设计的类, 该类实现 PrivilegedAction 接口。

如果不使用线程的当前方法,则可以使用另一种调用方法 Object doAsPrivileged(Subject, PrivilegedAction action, AccessControlContext acc) 来指定访问控制环境。它的特殊用法是将 AccessControlContext 设置为空, 在 doAsPrivileged 调用发生时,可以使调用栈短路,并且在 PrivilegedAction 对象中时,允许增加 权限。稍后,当对象返回到调用程序时,将减少权限。本教程稍后将说明这两种技术。

doAsdoAsPrivileged 方法形式上都允许抛出 PrivilegedActionException

权限

Java 平台有许多用于控制对系统资源的访问的内置权限。 例如:

grant signedBy "Brad", codeBase "http://www.bradrubin.com" {
       permission java.io.FilePermission "/tmp/abc", "read";
};

允许由“Brad”签名的并从“http://www.bradrubin.com”装入的代码读取 /tmp/abc 目录。有关 Java 权限的完整列表,请参阅参考资料

创建您自己的权限

Java 平台允许您创建自己的权限对象。与正规的权限相似, 可以将这些对象放在策略文件中,并在部署时配置它们。 为了进行演示,请查看下面的 PersonnelPermission 。 稍后,我们将使用这些代码以允许访问一些敏感的职员信息操作的代码。

import java.security.*;
//
// Implement a user defined permission for access to the personnel
// code for this example
public class PersonnelPermission extends
BasicPermission {

     public PersonnelPermission(String name) {
       super(name);
     }

     public PersonnelPermission(String name, String action) {
       super(name);
     }
}

对于上面的权限,您应该注意以下几点:
第一,构造器使用用户定义的特权名称(在这个示例中,只有一种名为
access 的类型)。另外一个构造器使用名为 action 的附加的改进的参数,虽然这里不使用它。对于这个示例,将使用 BasicPermission 类。
如果我们需要更多特性,可以使用 Permission 类。

策略文件

策略文件是控制对系统资源(包括敏感代码)访问的主要机制。 本示例中的策略文件名为 jaas.policy,并且在 Java 命令行中由特性 -Djava.security.policy==jaas.policy 指定。双等于号(== )表明将替换系统策略文件, 而不是添加到系统策略文件权限中。下面是我们正在本教程中使用的 jaas.policy 文件:

grant {
     permission javax.security.auth.AuthPermission "createLoginContext";
     permission javax.security.auth.AuthPermission "doAs";
     permission javax.security.auth.AuthPermission "doAsPrivileged";
     permission javax.security.auth.AuthPermission "modifyPrincipals";
     permission javax.security.auth.AuthPermission "getSubject"; };

grant      principal PrincipalImpl "Brad" {
     permission PersonnelPermission "access";
};

为了使 JAAS 机制自举,系统必须有某些特权 — 即示例中的前五个。 通过这些适当的权限,将访问权 PersonnelPermission (用户定义的权限)授予“Brad”主体。

JAAS 主程序示例

下面是这个示例的主应用程序(从命令行调用)。 它实例化登录环境、然后登录、尝试执行两个敏感对象(一个对象使用程序性授权,另一个对象使用声明性授权),最后注销。 接下来,我们将更深入地研究主程序的两个元素:程序性授权和声明性授权。

import java.security.*;
import javax.security.auth.*;
import javax.security.auth.callback.*;
import javax.security.auth.login.*;
//
// This is the main program in the JAAS Example.  It creates a Login
// Context, logs the user in based on the settings in the Login
// Configuration file,and calls two sensitive pieces of code, the
// first using programmatic authorization, and the second using
// declarative authorization.
public class JAASExample {

     static LoginContext lc = null;

     public static void main( String[] args) {
       //
       // Create a login context
       try {
         lc = new LoginContext("JAASExample",
            new UsernamePasswordCallbackHandler());
       } catch (LoginException le) {
         System.out.println( "Login Context Creation Error" );
         System.exit(1);
       }
       //

       // Login
       try {
         lc.login();
       } catch (LoginException le) {
         System.out.println( "\nOVERALL AUTHENTICATION FAILED\n" );
         System.exit(1);
       }
       System.out.println( "\nOVERALL AUTHENTICATION SUCCEEDED\n" );
       System.out.println( lc.getSubject() );
       //
       // Call the sensitive PayrollAction code, which uses programmatic
       // authorization.
       try {
         Subject
.doAs( lc.getSubject(), new PayrollAction() );
       } catch (AccessControlException e) {
         System.out.println( "Payroll Access DENIED" );
       }
       //
       // Call the sensitive PersonnelAction code, which uses declarative
       // authorization.
       try {
         Subject
.doAsPrivileged( lc.getSubject(), new PersonnelAction(), null );
       } catch (AccessControlException e) {
         System.out.println( "Personnel Access DENIED" );
       }
       try {
         lc.logout();
       } catch (LoginException le) {
         System.out.println( "Logout FAILED" );
         System.exit(1);
       }
       System.exit(0);

     }
}

程序性授权示例

在这个示例中,我们将了解如何编码程序权限决定。PrivilegedAction 类由主 JAASExample 程序的 doAs 方法调用,因此当它输入 run 方法时,经认证的 Subject 被绑定到线程上的应用程序环境。

我们从访问控制器检索当前的 Subject ,遍历任何包含的经认证的 Principal ,以查找“joeuser”。如果找到他,则可以进行敏感的操作并返回。 如果找不到,我们抛出一个 AccessControlException 。显然,在现实生活中我们应该使用更易于管理且可伸缩的技术, 而不是将用户名直接硬编码到应用程序中。

import java.io.*;
import java.security.*;
import javax.security.auth.*;
import javax.security.auth.login.*;
import java.util.*;
//
// This class is a sensitive Payroll function that demonstrates the
// use of programmatic authorization which only allows a subject
// that contains the principal "joeuser" in
class PayrollAction implements PrivilegedAction {
     public Object run() {
       // Get the passed in subject from the DoAs
       AccessControlContext context = AccessController.getContext();
       Subject
 subject = Subject
.getSubject(context );
       if (subject == null ) {
         throw new AccessControlException("Denied");
       }
       //
       // Iterate through the principal set looking for joeuser.  If
       // he is not found,
       Set principals = subject.getPrincipals();
       Iterator iterator = principals.iterator();
       while (iterator.hasNext()) {
         PrincipalImpl principal = (PrincipalImpl)iterator.next();
         if (principal.getName().equals( "joeuser" )) {
           System.out.println("joeuser has Payroll access\n");
           return new Integer(0);
         }
       }
       throw new AccessControlException("Denied");
     }
}

声明性授权示例

在这个示例中,我们将通过使用用户定义的权限 PersonnelPermission , 来演示如何用策略文件中的权限授予以声明性授权的方式来控制授权检查。 我们只询问 AccessController 是否已经授予这个权限, 如果没有授予,它抛出一个 AccessControlException ,否则,如果已经授予,则保持运行。 我们用主 JAASExample 代码中的 doAsPrivileged 调用和空访问控制环境来调用 这个 PrivilegedAction ,以使调用时调用栈短路。 因为在将 SubjectdoAsPrivileged 调用中的环境结合之前,Subject 不是环境的一部分,也未经 grant 语句授权,而且还因为使用了“最小特权”和权限的交集, 所以这是必需的,否则将不允许提高权限的级别。

import java.io.*;
import java.security.*;
//
// This class is a sensitive Personnel function that demonstrates
// the use of declarative authorization using the user defined
// permission PersonnelPermission, which throws an exception
// if it not granted
class PersonnelAction implements PrivilegedAction {
     public Object run() {
       AccessController.checkPermission(new PersonnelPermission("access"));
       System.out.println( "Subject
 has Personnel access\n");
       return new Integer(0);
     }
}

运行示例

设计 JAASExample 应用程序的目的是为了演示几种认证和授权技术以及一些配置设置的影响。 用下列语句开始运行该示例,可以在教程源文件(请参阅参考资料)中的文件 run.bat 中找到这些语句。

 java -Djava.security.manager
-Djava.security.auth.login.config==login.config
-Djava.security.policy==jaas.policy JAASExample

语句指示系统的缺省安全性管理器使用名为 login.config 的登录配置文件,
使用名为 jaas.policy 的安全性策略文件,最后运行主应用程序 JAASExample。注:
双等于号(== )表明系统缺省登录配置和策略文件 应该添加到已在这里列出的各项中。
一个等于号(= )表明应该将文件与系统缺省值并置。
示例结果和说明

下面是运行 JAASExample 的结果:

AlwaysLoginModule Login
Username? Brad

Login: AlwaysLoginModule SUCCESS

PasswordLoginModule Login
Username? joeuser
Password? joepw

Login: PasswordLoginModule Username Matches
Login: PasswordLoginModule Password Matches
Login: PasswordLoginModule SUCCESS
Commit: AlwaysLoginModule SUCCESS
Commit: PasswordLoginModule SUCCESS

OVERALL AUTHENTICATION SUCCEEDED

Subject
:
           Principal
: Brad
           Principal
: joeuser

joeuser has Payroll access

Subject
 has Personnel access

Logout: AlwaysLoginModule SUCCESS
Logout: PassswordLoginModule SUCCESS

下面详尽地描述了上面结果中的正常执行情况:

  1. login.config 定义两个登录模块;AlwaysLoginModule 是必需的。它先运行。
  2. AlwaysLoginModule 在登录阶段启动,它调用回调处理程序来获取用户名(Brad )。登录成功。
  3. 第二个登录模块 PasswordLoginModule 是可选的。它接下来运行,调用回调处理程序以获取用户名(joeuser )和密码(joepw ),两者都是匹配的。该登录也是成功的。
  4. 因为必需的和可选的模块都成功,所以在两个登录模块上同时调用 commit , 并且整个认证成功。结果,Subject 同时包含两个 PrincipalBradjoeuser
  5. 使用程序性授权的工资单程序检查 joeuser 是否在 SubjectPrincipal 集中,如果是,授予它访问权。
  6. 使用声明性授权的职员信息程序查看 jaas.policy 文件中是否有授权语句,授予 Brad PersonnelPermission 的特权,所以它也成功。
  7. 两个登录模块同时注销。

失败的认证

只是为了好玩,让我们看一下当我们出差错时会发生什么情况。 在下面的示例中,设置是相同的,但我们将为 joeuser 输入一个错误密码。请亲自检查下面的输出,看一下它与上面的结果有何不同。

AlwaysLoginModule Login
Username? Brad

Login: AlwaysLoginModule SUCCESS

PasswordLoginModule Login
Username? joeuser
Password? wrongpw

Login: PasswordLoginModule Username Matches
Login: PasswordLoginModule Password Mismatch
Login: PasswordLoginModule FAIL
Commit: AlwaysLoginModule SUCCESS
Commit: PasswordLoginModule FAIL

OVERALL AUTHENTICATION SUCCEEDED

Subject
:
           Principal
: Brad

Payroll Access DENIED
Subject
 has Personnel access

Logout: AlwaysLoginModule SUCCESS
Logout: PasswordLoginModule SUCCESS

正如您所看到的那样,PasswordLoginModule 登录已经失败。 但是,因为这个模块在 login.config 文件中配置为 optional , 所以总体认证仍是成功的。区别在于只有 Brad Principal 被添加到 Subject 中。 工资单程序找不到 joeuser Principal ,所以访问被拒绝。 职员信息程序可以将 Brad PrincipalBrad 授权语句匹配, 所以它被成功添加并授予访问权。

在接下来的几页中,我们将对 login.config 文件的配置作一些修改,然后检查每种新配置的结果。

变体 1:登录配置

首先,让我们看一下当我们将 login.config 文件更改为: 为了认证成功,两个 登录模块都是必需的,会是什么情况。新的 config 文件是:

JAASExample {
      AlwaysLoginModule required;
      PasswordLoginModule required;
};

这里是结果输出:


AlwaysLoginModule Login
Username? Brad

Login: AlwaysLoginModule SUCCESS

PasswordLoginModule Login
Username? joeuser
Password? wrongpw

Login: PasswordLoginModule Username Matches
Login: PasswordLoginModule Password Mismatch
Login: PasswordLoginModule FAIL
Abort: AlwaysLoginModule SUCCESS
Logout: AlwaysLoginModule SUCCESS
Abort: PasswordLoginModule FAIL

OVERALL AUTHENTICATION FAILED

joeuser 输入错误密码时,PasswordLoginModule 就会象先前那样失败。 然而,因为该模块是必需的,所以运行异常终止阶段并且总体认证失败。 不执行任何敏感代码。

变体 2:PAM 的能力

该变体是为演示可插入的认证模块的实用程序而设计的。我们回到原始的 login.config 文件,即 AlwaysLoginModule 是必需的,而 PasswordLoginModule 是可选的, 然后将 NTLoginModule (或适用于您的平台的任何其它模块)添加到该文件。 新模块将是 required 。修改后的 login.config 文件应该看起来如下:

JAASExample {
      AlwaysLoginModule required;
      PasswordLoginModule optional;
      com.sun.security.auth.module.NTLoginModule required;
};

接下来,运行示例。在下面的输出中,您将看到已经添加了一个新的认证方法以及几个全新的 Principal (和一个公用凭证)。

AlwaysLoginModule Login
Username? Brad

Login: AlwaysLoginModule SUCCESS

PasswordLoginModule Login
Username? joeuser
Password? joepw

Login: PasswordLoginModule Username Matches
Login: PasswordLoginModule Password Matches
Login: PasswordLoginModule SUCCESS
Commit: AlwaysLoginModule SUCCESS
Commit: PasswordLoginModule SUCCESS

OVERALL AUTHENTICATION SUCCEEDED

Subject
:
           Principal
: Brad
           Principal
: joeuser
           Principal
: NTUserPrincipal: Brad
           Principal
: NTDomainPrincipal: WORKGROUP
           Principal
: NTSidUserPrincipal:
S-1-5-21-2025429265-1580813891-854245398-1004
           Principal
: NTSidPrimaryGroupPrincipal:
S-1-5-21-2025429265-1580418891-85 4245398-513
           Principal
: NTSidGroupPrincipal:
S-1-5-21-2025429265-1580818891-854245398-513
           Principal
: NTSidGroupPrincipal: S-1-1-0
           Principal
: NTSidGroupPrincipal: S-1-5-32-544
           Principal
: NTSidGroupPrincipal: S-1-5-32-545
           Principal
: NTSidGroupPrincipal: S-1-5-5-0-49575

           Principal
: NTSidGroupPrincipal: S-1-2-0
           Principal
: NTSidGroupPrincipal: S-1-5-4
           Principal
: NTSidGroupPrincipal: S-1-5-11
           Public Credential: NTNumericCredential: 1240

joeuser has Payroll access

Subject
 has Personnel access

Logout: AlwaysLoginModule SUCCESS
Logout: PasswordLoginModule SUCCESS

更酷的是,我们甚至不必自己改动我们的应用程序代码。上面所有的更改都是由本机 OS 认证机制完成的。 这应该向您暗示了 PAM 的能力。

变体 3:策略文件配置

在最后一个变体中,我们将查看当修改访问控制策略时会发生什么情况。 我们从修改原始 login.config 中的 grant 文件开始,以便 joeuser (而不是 Brad )有 PersonnelPermission ,如下所示:

grant Principal
 PrincipalImpl "joeuser" {
     permission PersonnelPermission "access";
};

接下来,运行应用程序,为 joeuser 输入错误密码。结果如下所示:

AlwaysLoginModule Login
Username? Brad

Login: AlwaysLoginModule SUCCESS

PasswordLoginModule Login
Username? joeuser
Password? wrongpw

Login: PasswordLoginModule Username Matches
Login: PasswordLoginModule Password Mismatch
Login: PasswordLoginModule FAIL
Commit: AlwaysLoginModule SUCCESS
Commit: PasswordLoginModule FAIL

OVERALL AUTHENTICATION SUCCEEDED

Subject
:
           Principal
: Brad

Payroll Access DENIED
Personnel Access DENIED
Logout: AlwaysLoginModule SUCCESS
Logout: PasswordLoginModule SUCCESS

正如您所看到,SubjectPrincipal 集中只有 Brad 。 工资单访问和职员信息访问的尝试都已失败。为什么?第一次尝试失败是因为没有名为 joeuserPrincipal ,第二次尝试失败是因为对于 Brad 没有授予权限语句。

不要在此止步

在本章中,我们已经将所有 JAAS 认证和授权片段合在一起来说明完整的 JAAS 应用程序的运行情况。 我们还对应用程序做了几次改动以观察它实际上发生了什么以及该体系结构在应用程序安全性方面有多灵活。

为了扩展您在这里学到的知识,您应该继续使用 JAAS 并查看当尝试不同的登录配置时会发生什么情况。 例如,如果在您的安装中运行着 Kerberos,尝试运行 Kerberos 登录模块。

参考资料

下载

  • 下载本教程中使用的完整源代码和类。
  • 可从 Sun Microsystems 获得 Java 2 平台,标准版 (http://java.sun.com/j2se/)。

文章、教程和其它在线参考资料

  • 请阅读由 Brad Rubin 编写的本教程系列的第 1 部分,“Crypto basics ,”(http://www-106.ibm.com/developerworks/education/r-jsec1.html)。
  • 请参阅 Java 开发人员连接(Java Developer Connection), 以获取完整的 Java 权限 (http://java.sun.com/j2se/1.4/docs/guide/security/permissions.html)的列表。
  • 虽然本教程中未论及新近与 JDK 1.4 一起提供的 Java 通用安全性服务(Java General Security Service(JGSS)), 但它提供了用于在应用程序之间安全地交换消息的通用框架。Sun 最新发布的白皮书 (http://java.sun.com/j2se/1.4/docs/guide/security/jgss/single-signon.html)讨论了如何使用 JAAS、JGSS 和 Kerberos 来提供单点登录应用程序安全性。
  • Sun 还拥有几个描述 JAAS 和 JGSS 的不同使用方法和过程的教程和用户指南 (http://java.sun.com/j2se/1.4/docs/guide/security/jgss/tutorials/)。有一个极好的参考指南描述了 when to use JGSS versus JSEE (http://java.sun.com/j2se/1.4/docs/guide/security/jgss/tutorials/JGSSvsJSSE.html)。
  • 请参阅 Sun Microsystems 的 Java Security 站点 (http://java.sun.com/security),学习更多有关最新 Java 安全性技术的知识。
  • Joseph Sinclair 在系列文章“Securing systems ”中, 提供了一篇“A three-pronged solution for identifying users”(developerWorks ,2001 年 6 月,http://www-106.ibm.com/developerworks/library/j-secure/index.html)。
  • 一旦您掌握了基本知识,Carlos Fonseca 将向您展示如何“Extend JAAS for class instance-level authorization ”(developerWorks ,2002 年 4 月,http://www-106.ibm.com/developerworks/library/j-jaas/)。
  • 在“Enhance Java GSSAPI with a login interface using JAAS ”中,Thomas Owusu 提供了有关凭证和秘钥的一些颇有见地的观点(developerWorks ,2001 年 11 月,http://www-106.ibm.com/developerworks/library/j-gssapi/)。
  • 查找 WebSphere Portal Server 1.2 如何实现 JAAS 和单点登录安全性 (WebSphere 开发者园地,2001 年 10 月)。

书籍

相关推荐

Global site tag (gtag.js) - Google Analytics