Java 安全性,第 2 部分: 认证与授权

来源:互联网 发布:cms傻瓜式建站系统 编辑:程序博客网 时间:2024/06/06 08:57

概念性的概述

认证与授权

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

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

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

认证元素

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

  • 您知道什么。该类别包括个别人知道而其它人一般不知道的信息。示例包括 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 程序的图。下一页将描述操作流。

图 1

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)用来决定是否可以继续执行。

JAAS 中的认证

概述

本章中,我们将集中讨论 JAAS 中的认证元素。我们将从描述简单的登录和认证过程开始,它将为您提供 JAAS 认证体系结构的高级别视图。接着,我们将详细讨论体系结构的每一部分。本章结束时,您将有机会仔细地研究两个登录模块的代码。

如果您还没有下载本教程的源代码JavaSecurity2-source.jar,请您现在就开始下载。该源代码能更好地说明后面讨论中概述的步骤。

Subject 和 Principal

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

Principal 不是持久性的,所以每次用户登录时都必须将它们添加到 Subject。Principal 作为成功认证过程的一部分被添加到 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 classUsernamePasswordCallbackHandler 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 对象并将它添加到 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 classPasswordLoginModule 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;     }}

JAAS 中的授权

概述

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

在本章中,您将了解 JAAS 的用于控制对敏感代码访问的机制。我们将首先描述授权在 JAAS 中是如何工作的,然后继续更深入地描述授权框架的每个组件。在本章的最后,我们将给出在较大型的运行示例中使用的一些代码样本,它们演示了程序性授权和声明性授权技术。读完本章之后,您应该清楚地知道 JAAS 的认证和授权机制如何一起工作来保护基于 Java 的系统。

访问控制和权限

因为执行线程可以跨越多个具有不同环境特征的模块,所以 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 extendsBasicPermission {     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 PayrollActionimplements 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,以使调用时调用栈短路。因为在将 Subject 与doAsPrivileged 调用中的环境结合之前,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);     }

JAAS 示例

概述

最后,我们开始讨论本教程最有趣的部分:边学边做。我们从运行比较全面的 JAASExample 应用程序开始,它包含我们已经在教程中讨论过的所有认证和授权机制(和代码)。运行该示例并查看了第一次配置的结果之后,我们将研究一些不同的配置选项并马上检查结果。在本教程的这最后一章结束时,您将初次体验 JAAS 编程的乐趣。

运行示例

设计 JAASExample 应用程序的目的是为了演示几种认证和授权技术以及一些配置设置的影响。用下列语句开始运行该示例,可以在教程源文件(请参阅参考资料JavaSecurity2-source.jar)中的文件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 LoginUsername? BradLogin: AlwaysLoginModule SUCCESSPasswordLoginModule LoginUsername? joeuserPassword? joepwLogin: PasswordLoginModule Username MatchesLogin: PasswordLoginModule Password MatchesLogin: PasswordLoginModule SUCCESSCommit: AlwaysLoginModule SUCCESSCommit: PasswordLoginModule SUCCESSOVERALL AUTHENTICATION SUCCEEDEDSubject:           Principal: Brad           Principal: joeuserjoeuser has Payroll accessSubject has Personnel accessLogout: AlwaysLoginModule SUCCESSLogout: PassswordLoginModule SUCCESS

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

  1. login.config 定义两个登录模块;AlwaysLoginModule 是必需的。它先运行。

  2. AlwaysLoginModule 在登录阶段启动,它调用回调处理程序来获取用户名(Brad)。登录成功。

  3. 第二个登录模块 PasswordLoginModule 是可选的。它接下来运行,调用回调处理程序以获取用户名(joeuser)和密码(joepw),两者都是匹配的。该登录也是成功的。

  4. 因为必需的和可选的模块都成功,所以在两个登录模块上同时调用 commit,并且整个认证成功。结果,Subject 同时包含两个 Principal:Bradjoeuser

  5. 使用程序性授权的工资单程序检查 joeuser 是否在 Subject 的Principal 集中,如果是,授予它访问权。

  6. 使用声明性授权的职员信息程序查看 jaas.policy 文件中是否有授权语句,授予 Brad PersonnelPermission 的特权,所以它也成功。

  7. 两个登录模块同时注销。

失败的认证

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

AlwaysLoginModule LoginUsername? BradLogin: AlwaysLoginModule SUCCESSPasswordLoginModule LoginUsername? joeuserPassword? wrongpwLogin: PasswordLoginModule Username MatchesLogin: PasswordLoginModule Password MismatchLogin: PasswordLoginModule FAILCommit: AlwaysLoginModule SUCCESSCommit: PasswordLoginModule FAILOVERALL AUTHENTICATION SUCCEEDEDSubject:           Principal: BradPayroll Access DENIEDSubject has Personnel accessLogout: AlwaysLoginModule SUCCESSLogout: 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 LoginUsername? BradLogin: AlwaysLoginModule SUCCESSPasswordLoginModule LoginUsername? joeuserPassword? wrongpwLogin: PasswordLoginModule Username MatchesLogin: PasswordLoginModule Password MismatchLogin: PasswordLoginModule FAILAbort: AlwaysLoginModule SUCCESSLogout: AlwaysLoginModule SUCCESSAbort: PasswordLoginModule FAILOVERALL 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 LoginUsername? BradLogin: AlwaysLoginModule SUCCESSPasswordLoginModule LoginUsername? joeuserPassword? joepwLogin: PasswordLoginModule Username MatchesLogin: PasswordLoginModule Password MatchesLogin: PasswordLoginModule SUCCESSCommit: AlwaysLoginModule SUCCESSCommit: PasswordLoginModule SUCCESSOVERALL AUTHENTICATION SUCCEEDEDSubject:           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: 1240joeuser has Payroll accessSubject has Personnel accessLogout: AlwaysLoginModule SUCCESSLogout: PasswordLoginModule SUCCESS

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

变体 3:策略文件配置

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

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

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

AlwaysLoginModule LoginUsername? BradLogin: AlwaysLoginModule SUCCESSPasswordLoginModule LoginUsername? joeuserPassword? wrongpwLogin: PasswordLoginModule Username MatchesLogin: PasswordLoginModule Password MismatchLogin: PasswordLoginModule FAILCommit: AlwaysLoginModule SUCCESSCommit: PasswordLoginModule FAILOVERALL AUTHENTICATION SUCCEEDEDSubject:           Principal: BradPayroll Access DENIEDPersonnel Access DENIEDLogout: AlwaysLoginModule SUCCESSLogout: PasswordLoginModule SUCCESS

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

不要在此止步

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

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

结束语

结束语

本教程介绍了 Java 平台认证与授权服务,我们称之为 JAAS。除了熟悉所有基本的 JAAS 组件外,您还了解了几个登录模块的实用性介绍并学习了在命令行上使用 JAAS 的基础知识。而且,您还有机会运行实际的 JAAS 应用程序并尝试几种成功的和不成功的配置。

阅读完本教程之后,您应该发现您自己能很自信地继续研究 JAAS 编程框架。将 JAAS 的认证和授权技术与本教程第 1 部分中讨论的密码技术结合起来,您就能够构造并实现大量的应用程序安全性解决方案。为了使您的学习更进一步,您应该继续使用 JAAS,研究各种不同的登录模块、配置和安全性案例。