认证概述
本章中,我们将集中讨论 JAAS 中的认证元素。我们将从描述简单的登录和认证过程开始,它将为您提供 JAAS 认证体系结构的高级别视图。接着,我们将详细讨论体系结构的每一部分。本章结束时,您将有机会仔细地研究两个登录模块的代码。
如果您还没有下载本教程的源代码,请您现在就开始下载。该源代码能更好地说明后面讨论中概述的步骤。
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 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;
//
// 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 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 = ' ';
}
password = null;
}