本文翻译自:
http://java.sun.com/developer/technicalArticles/Security/jaasv2/
传统的JAVA安全机制没有提供必要的架构支持传统的认证和授权;在J2SE里的安全是基于公钥密码体系和代码签名。也就是说,认证是基于在JVM里执行代码的思想,并且没有对资源请求提供策略。而且授权也是基于这样的概念--代码试图去使用一个计算机资源。Java认证和授权服务(JAAS)也就被设计成去应付这些缺点。
JAAS使用基于用户的的访问控制增加了这个已经存在的基于代码的访问控制机制,也提高了认证能力。这样你赋予的权限不仅是什么代码在运行,而且可以是谁在运行它。这篇文章:
- 讨论基于代码的认证的缺点
- 提供JAAS的概览
- 展示了一个加强的代码的认证例子
- 展示了一个加强的代码的授权例子
- 给予了在开发认证和授权服务时调用成功的感觉。
- 提供了能够适应与你所拥有的应用程序的简单代码
- 提供了一个开始学习JAAS的指导
目的
传统的操作系统(例如UNIX)通过多种challenge-response机制对主体或实体进行认证。用户名和密码组合是属于最常见的了。这种技术也被用来使用HTTP基本认证方案保护资源。无论怎样,challenge可以做得更复杂:例如,可以加密信息或者依赖与特定信息的持有者(比如妈咪的小名或者一个所选问题的答案)。然后基于这种challenge类型的响应必须是有效的。
此外,大多数操作系统在一个实体或主体上的和资源列表上的基本授权是授予实体或主体去使用。例如,当一个用户尝试读或者写一个文件时,授权机制就会去验证当前正在执行的主体是否有权限去访问这个资源。
JAAS概览
JAAS分为2个部分:认证(authentication)和授权(authorization). 也就是说,认证和授权两个都可以使用。
- 对于认证用户可以安全地决定谁正在执行代码,即使代码是一个单独的应用程序、一个applet、一个企业级JavaBean或者一个servlet。
- 对于用户的授权,可以确认他们是否有必要的权限去执行他们的操作。
认证是基于Plugable Authentication Modules(PAMs)(可插入认证模型),它使用一个框架,用于客户端和服务端。,认证部分是一个使用存在于J2SE中策略文件的认证方案的扩展。在一个可插入的方式中执行认证使Java应用程序可以去依赖于底层的安全机制。这样做有个优点,就是新的或者修正的认证机制可以方便地插入,而不用对应用程序本身做修改。
授权是一个基于存在的策略文件的机制的扩展,基于策略文件的机制被用作指定一个应用程序(或可执行代码)能或者不能做什么操作。它是基于保护域的。也就是说,这种机制授权基于代码来自哪里,而不是基于谁在执行代码。用JAAS permissions或者访问控制不仅能控制在运行什么代码,也能控制谁正在运行它。
注意:JAAS在J2SE 1.3.x是作为一个可选的包,但是到J2SE 1.4 已经被完全整合了。
java.security.Policy API已经被更新了,能够在策略文件里支持基于主体的查询和基于主体的授权,等下你就可以看到
使用JAAS认证(Authentication)
客户端通过一个LoginContext对象与JAAS相互作用,这个LoginContext对象提供了一种开发应用程序的方式,它不依赖于底层的认证技术。LoginContext是javax.security.auth.login包里的一个类,它描述了用于验证对象(subjects)的方法。Subject就是在某个你想去认证和分配访问权限的系统里的一个标识。一个主体(subject)可能是一个用户、一个进程或者是一台机器,它用javax.security.auth.Subject类表示。由于一个Subject可能涉及多个授权(一个网上银行密码和另一个电子邮件系统),java.security.Principal就被用作在那些关联里的标识。也就是说,该Principal接口是一个能够被用作代表某个实体、公司或者登陆ID的抽象概念。一个Subject可能包含多个Principles. 稍后将有一个示例类实现了这个Principal接口。
LoginContext对象调用负责实现和执行认证的LoginModules。LoginModule接口(在javax.security.auth.spi 包里)必须让认证技术的提供者去实现,并且能够被应用程序指定提供一个特定认证类型。LoginContext用来读取Configuaration和实例化特定的LoginModules.
Configuaration被某个特定的应用程序用作指定认证技术或者LoginModule。因此,不同的LoginModules能够被应用到某个应用程序而不用对这个应用程序做任何的代码修改。
简单代码1展示了一个简单的JAAS客户端。我已经使用了要调用LoginModules的LoginContext去执行认证,并且用"WeatherLogin"这个名字实例化了LoginContext, 还回调了"MyCallbackHandler"(实现见简单代码2)处理程序。这个名字在配置文件里将被用作索引,决定应该使用哪个LoginModule. 当你看这个配置文件(见简单代码5)后会变得更加清楚。回调处理程序传给了底层的LoginModule,因此他们能够通过提示用户名/密码同用户进行交流和作用,例如:通过文本或者图形用户接口。一旦LoginContext已经被实例化了,login方法就被调用去登陆。
简单代码1:MyClient.java
import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
public class MyClient {
public static void main(String argv[]) {
LoginContext ctx = null;
try {
ctx = new LoginContext("WeatherLogin", new MyCallbackHandler());
} catch(LoginException le) {
System.err.println("LoginContext cannot be created. "+ le.getMessage());
System.exit(-1);
} catch(SecurityException se) {
System.err.println("LoginContext cannot be created. "+ se.getMessage());
}
try {
ctx.login();
} catch(LoginException le) {
System.out.println("Authentication failed. " + le.getMessage());
System.exit(-1);
}
System.out.println("Authentication succeeded.");
System.exit(-1);
}
}
一个基于JAAS的应用程序实现了CallbackHandler接口,因此它能够提示用户去输入特定的认证信息,比如用户名或者密码,或者显示错误或者警告信息。底层安全服务可能要求通过传递单个的callbacks到回调处理程序。基于传递的callbacks,回调处理程序决定怎样去获取和显示信息。例如,如果底层服务需要一个用户名和密码认证一个用户,它可以使用NameCallback和PasswordCallback. 其他的callbacks类都在javax.security.auth.callback里,包括:
- ChoiceCallback (显示一个选择列表)
- ConfirmationCallback (询问 YES/NO, OK/CANCEL)
- LanguageCallback (用作区域化的Locale)
- TextInputCallback (获取普通的文本信息)
- TextOutputCallback (显示信息,警告和错误信息)
实现CallbackHandler接口意味着你需要实现handler方法,以获取或者显示在提供的callbacks里要求的信息。简单代码2是一个简单的实现。注意在这里我使用NameCallback与用户关联。
简单代码2:MyCallbackHandler.java
import java.io.*;
import javax.security.auth.*;
import javax.security.auth.callback.*;
public class MyCallbackHandler implements CallbackHandler {
public void handle(Callback callbacks[]) throws IOException, UnsupportedCallbackException {
for(int i=0;i<callbacks.length;i++) {
if(callbacks[i] instanceof NameCallback) {
NameCallback nc = (NameCallback) callbacks[0];
System.err.print(nc.getPrompt());
System.err.flush();
String name = (new BufferedReader(new InputStreamReader(System.in))).readLine();
nc.setName(name);
} else {
throw(new UnsupportedCallbackException(callbacks[i], "Callback handler not support"));
}
}
}
}
现在,我们看一个LoginModule的简单实现。注意在真实的应用程序开发中,我们不必要自己去实现LoginMoudule。我们可以使用第三方的login模型并把他们应用到我们的程序。例如,Sum Microsystems 提供了几个LoginModule,包括:JndiLoginModule、KeyStoreLoginModule、Krb5LoginModule、NTLoginModule、UNIXLoginModule. 如果你愿意学习怎样去使用这些login模型,请查看本文的参考信息部分。
无论怎样,简单代码3展示了一个LoginModule简单的实现. 这个例子是非常简单的,因为他仅仅有一个认证字符串和一个Principal "SunnyDay", 两个都是硬编码。如果去login,系统将显示"What is the weather like today?", 如果答案是"Sunny", 用户就能通过。注意MyCallbackHandler是怎样被用在login方法里的。除了login方法,你必须实现其他四个方法:initialize, commit, abort, and logout. 这些方法将被LoginContext用在接下来的流程里。
- initialize: 这个方法的目的就是用有关的信息去实例化这个LoginModule。如果login成功,在这个方法里的Subject就被用在存储Principals和Credentials. 注意这个方法有一个能被用作输入认证信息的CallbackHandler。在这个例子里,我没有用CallbackHandler. CallbackHandler是有用的,因为它从被用作特定输入设备里分离了服务提供者。
- login: 请求LoginModule去认证Subject. 注意此时Principal还没有被指定。
- commit: 如果LoginContext的认证全部成功就调用这个方法。
- abort: 通知其他LoginModule供应者或LoginModule模型认证已经失败了。整个login将失败。
- logout: 通过从Subject里移除Principals和Credentials注销Subject。
(注意:出于格式考虑,这篇文章的一些界线和例子代码已经被分割了)。
简单代码3:WeatherLoginModule.java
import java.io.*;
import java.util.*;
import java.security.Principal;
import javax.security.auth.Subject;
import javax.security.auth.callback.*;
import javax.security.auth.spi.LoginModule;
import javax.security.auth.login.LoginException;
public class WeatherLoginModule implements LoginModule {
private Subject subject;
private ExamplePrincipal entity;
private CallbackHandler callbackhandler;
private static final int NOT = 0;
private static final int OK = 1;
private static final int COMMIT = 2;
private int status;
public void initialize(Subject subject, CallbackHandler//
callbackhandler, Map state, Map options) {
status = NOT;
entity = null;
this.subject = subject;
this.callbackhandler = callbackhandler;
}
public boolean login() throws LoginException {
if(callbackhandler == null) {
throw new LoginException("No callback handler is available");
}
Callback callbacks[] = new Callback[1];
callbacks[0] = new NameCallback("What is the weather like today?");
String name = null;
try {
callbackhandler.handle(callbacks);
name = ((NameCallback)callbacks[0]).getName();
} catch(java.io.IOException ioe) {
throw new LoginException(ioe.toString());
} catch(UnsupportedCallbackException ce) {
throw new LoginException("Error: "+ce.getCallback().toString());
}
if(name.equals("Sunny")) {
entity = new ExamplePrincipal("SunnyDay");
status = OK;
return true;
} else {
return false;
}
}
public boolean commit() throws LoginException {
if(status == NOT) {
return false;
}
if(subject == null) {
return false;
}
Set entities = subject.getPrincipals();
if(!entities.contains(entity)) {
entities.add(entity);
}
status = COMMIT;
return true;
}
public boolean abort() throws LoginException {
if((subject != null) && (entity != null)) {
Set entities = subject.getPrincipals();
if(entities.contains(entity)) {
entities.remove(entity);
}
}
subject = null;
entity = null;
status = NOT;
return true;
}
public boolean logout() throws LoginException {
subject.getPrincipals().remove(entity);
status = NOT;
subject = null;
return true;
}
}
正如你从简单代码3里所看到的,一个ExamplePrincipal类正在被使用。这个类是Principal接口的一个实现。
简单代码4展示了Principal接口的一个实现。
简单代码4:ExamplePrincipal.java
import java.security.Principal;
public class ExamplePrincipal implements Principal {
private final String name;
public ExamplePrincipal(String name) {
if(name == null) {
throw new IllegalArgumentException("Null name");
}
this.name = name;
}
public String getName() {
return name;
}
public String toString() {
return "ExamplePrinciapl: "+name;
}
public boolean equals(Object obj) {
if(obj == null) return false;
if(obj == this) return true;
if(!(obj instanceof ExamplePrincipal)) return false;
ExamplePrincipal another = (ExamplePrincipal) obj;
return name.equals(another.getName());
}
public int hasCode() {
return name.hashCode();
}
}
正如我前面提及的,LoginContext通过读取Configuration去决定那个LoginModule将被使用。login配置文件可以是一个文件或者是数据库。当前Sun Microsystems默认的实现是一个文件。一个login配置文件包含一个或者多个实体,这些实体指出了那些认证技术应该被拥有应用程序。简单代码5展示了一个login配置文件。
简单代码5:example.conf
WeatherLogin {
WeatherLoginModule required;
};
在这个配置文件里,实体名"WeatherLogin"就是被MyClient.java用作关联这个实体的名字。这里的这个实体指出WeatherLoginModule应该被用作执行认证。为了使整个认证成功,这个模型(module)的认证必须是成功的。用户输入了正确的信息就是成功的。
运行例子程序
- 在根目录下创建一个"auth"的目录;
- 把MyClient.java, WeatherLoginModule.java, ExamplePrincipal.java, example.conf复制到这个目录下;
- 编译所有的java文件:javac *.java;
- 使用下面的命令(指定了login配置文件)运行客户端,
prompt> java -Djava.security.auth.login.config=example.conf MyClient
你应该可以看到下面的输出(粗体文本是用户输入的)。
What is the weather like today?
gloomy
Authentication failed. Login Failure: all modules ignored
在用正确的输入运行客户端:
What is the weather like today?
Sunny
Authentication succeeded
安全管理器
在上面的例子里,默认是没有运行在安全管理器下的,因此所有操作都是允许的。为了保护资源,可以使用下面的命令去运行在安全管理器下:
C:\sun\code\auth>java -Djava.security.manager//
-Djava.security.auth.login.config=example.conf MyClient
LoginContext cannot be created. access denied//
(javax.security.auth.AuthPermission createLoginContext.WeatherLogin)
Exception in thread "main" java.lang.NullPointerException
at MyClient.main(MyClient.java:17)
你可以看到抛出了一个异常。默认的安全管理器不允许任何操作,因此login上下文没有被创建。要允许这些操作,你必须创建一个安全策略(安全策略是一个赋予代码能不能执行的权限的文本文件。简单代码6展示了一个简单的安全策略。目标createLoginContext使MyClient能够实例化一个login上下文。目标modifyPrincipals允许WeatherLoginModule用一个Principal去构造一个Subject.
简单代码6:policy.txt
grant codebase "file:./*" {
permission javax.security.auth.AuthPermission "createLoginContext";
permission javax.security.auth.AuthPermission "modifyPrincipals";
};
现在你就能够使用下面的命令来运行了,注意双等号(==)是用来覆盖默认安全策略的。
prompt>java -Djava.security.manager -Djava.security.policy==policy.txt
-Djava.security.auth.login.config==example.conf MyClient
What is the weather like today?
Sunny
Authentication succeeded
注意:LoginModule的实现代码和应用程序代码也可以放在一个jar文件里。更多相关信息,请看LoginModule Developer's Guide.
使用JAAS授权(Authorization)
JAAS授权继承了以代码为中心的JAVA安全体系结构(它使用一个安全策略指定什么样的访问权限授予执行中的代码。例如,在简单代码6的安全策略里,所有当前目录的的代码都被授权;不管代码有没有签名,或者谁在运行这些代码。JAAS也继承了以用户为中心(user-centric)的访问控制。许可权的赋予不仅是正在运行什么代码,而且也看谁在运行它。很快你将看到许可权能够在策略文件里被赋予去指定principals。
为了使用JAAS授权:
- 用户必须是我在上面认证过的;
- Subject的doAs(或者doAsPrivileged)方法必须被调用,它在反过来调用run方法(包含作为主体去执行的)。
基于这个,ExamplePrincipal.java, WeatherLoginModule.java和 example.conf文件仍然是同一个。
为了使客户端能够赋予用户权限,一旦认证成功(用户已经被认证了),认证了的主体就使用通过Subject subject=ctx.getSubject()来获取。然后通过传递给Subject.doAsPrivileged()一个认证了的主体和一个特权行动以及空AccessControlContext去调用它。这些改变在简单代码7是用高亮标识的。注意一旦执行了认证过程,我就调用ctx.logout是用户退出。
简单代码7: MyClient.java
import javax.security.auth.Subject;
import java.security.PrivilegedAction;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
public class MyClient {
public static void main(String argv[]) {
LoginContext ctx = null;
try {
ctx = new LoginContext("WeatherLogin", new MyCallbackHandler());
} catch(LoginException le) {
System.err.println("LoginContext cannot be created. "+ le.getMessage());
System.exit(-1);
} catch(SecurityException se) {
System.err.println("LoginContext cannot be created. "+ se.getMessage());
}
try {
ctx.login();
} catch(LoginException le) {
System.out.println("Authentication failed");
System.exit(-1);
}
System.out.println("Authentication succeeded");
Subject subject = ctx.getSubject();
PrivilegedAction action = new MyAction();
Subject.doAsPrivileged(subject, action, null);
try {
ctx.logout();
} catch(LoginException le) {
System.out.println("Logout: " + le.getMessage());
}
}
}
你可以看到,我们传递了一个action给doAsPrivileged,action就是用户被授权去执行的行为。简单代码8展示了MyAction.java, 它通过提供run方法的代码实现了PrivilegedAction接口,run方法包含了所有将用基于principal的认证检查的代码。现在,当执行doAsPrivileged方法时,它就会反过来调用在PrivilegedAction里的方法,去实例化主体其他方面剩余代码的执行。
在这个例子里,action就是去检查文件"max.txt"存在于当前的目录。
简单代码8:MyAction.java
import java.io.File;
import java.security.PrivilegedAction;
public class MyAction implements PrivilegedAction {
public Object run() {
File file = new File("max.txt");
if(file.exists()) {
System.out.println("The file exists in the current working directory");
} else {
System.out.println("The file does not exist in the current working directory");
}
return null;
}
}
现在我需要更新policy.txt文件。简单代码9展示了这些更改。
- 为了Subject类的doAsPrivileged方法,你需要有一个有doAsPrivileged目标的java.security.auth.AuthPermission.
- 添加了一个基于principal的授权;它声明了一个"SunnyDay"的pincipal被运行去读这个"max.txt"文件。
简单代码9:policy.txt
grant codebase "file:./*" {
permission javax.security.auth.AuthPermission "createLoginContext";
permission javax.security.auth.AuthPermission "modifyPrincipals";
permission javax.security.auth.AuthPermission
"doAsPrivileged";
};
grant codebase "file:./*", Principal ExamplePrincipal "SunnyDay" {
permission java.io.FilePermission "max.txt", "read";
};
现在当你运行这个应用程序,它将首先执行认证。如果认证失败,应用程序就停止,否则它将继续去检查这个文件。看下面简单的运行:
prompt>java -Djava.security.manager -Djava.security.policy==policy.txt//
-Djava.security.auth.login.config==example.conf MyClient
What is the weather like today?
sunny
Authentication failed
prompt>java -Djava.security.manager -Djava.security.policy==policy.txt//
-Djava.security.auth.login.config==example.conf MyClient
What is the weather like today?
Sunny
Authentication succeeded
The file does not exist in the current working directory
创建一个"max.txt"文件后再运行下这个客户端。
结束语
JAAS是一个能够认证和强制执行用户访问控制的服务的API集合。JAAS认证使用可插入的方式,它不依赖与底层的认证技术;JAAS授权使用以用户为中心的访问控制和认证能力提高了以原本以代码为中心的JAVA安全体系。