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

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

AlwaysLoginModule

AlwaysLoginModule 认证将始终成功, 所以实际上它仅用于通过 NameCallback 函数获取用户名。 假设其它登录模块都成功,AlwaysLoginModule 的 commit() 方法将创建一个带用户名的新 PrincipalImpl 对象并将它添加到 Subject 的 Principal 集中。注销将除去 Subject 的 Principal 集 中的 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 t
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值