需求
Java Authentication Authorization Service(JAAS,Java验证和授权API)提供了灵活和可伸缩的机制来保证客户端或服务器端的Java程序。
Java早期的安全框架强调的是通过验证代码的来源和作者,保护用户避免受到下载下来的代码的攻击。
JAAS强调的是通过验证谁在运行代码以及他/她的权限来保护系统面受用户的攻击。
它让你能够将一些标准的安全机制,例如Solaris NIS(网络信息服务)、Windows NT、LDAP(轻量目录存取协议),Kerberos等通过一种通用的,可配置的方式集成到系统中。
你是否曾经需要为一个应用程序实现登录模块呢?如果你是一个比较有经验的程序员,相信你这样的工作做过很多次,而且每次都不完全一样。你有可能把你的登录模块建立在Oracle数据库的基础上,也有可能使用的是NT的用户验证,或者使用的是LDAP目录。如果有一种方法可以在不改变应用程序级的代码的基础上支持上面提到的所有这一些安全机制,对于程序员来说一定是一件幸运的事。
设计
框架
- 应用程序层的代码只需要和LoginContext打交道。
- 在LoginContext之下是一组动态配置的LoginModule对象
- LoginModule对象使用相关的安全基础结构进行验证操作。
特性
-
可扩展
通过在应用程序和底层的验证和授权机制之间加入一个抽象层,通过一个可扩展的框架:服务提供者接口(Service Provider Interface,SPI)来保证程序独立于安全机制。
-
可堆叠
为了满足可插接性,JAAS是可堆叠的。在单一登录的情况下,一组安全模块可以堆叠在一起,然后被其他的安全机制按照堆叠的顺序被调用。
可堆叠的特性同Unix下的可堆叠验证模块(PAM,Pluggable Authentication Module)框架就非常相似。从事务的角度看,JAAS类似于两阶段提交(Two-Phase Commit,2PC)协议的行为(第一个阶段执行login按照顺序调用LoginModule执行验证,第二个阶段执行commit将Principal对象和凭证赋给Subject对象)。
api
核心JAAS类和接口。这些类可以被分为三种类型
普通类型 Subject,Principal,Credential
-
Subject
Subject类代表了一个验证实体,它可以是用户、管理员、Web服务,设备或者其他的过程。该类包含了三中类型的安全信息:
身份(Principals):由一个或多个Principal对象表示
公共凭证(Public credentials):例如名称或公共秘钥
私有凭证(Private credentials):例如口令或私有密钥
-
Principal
Principal对象代表了Subject对象的身份。它们实现了java.security.Principal和java.io.Serializable接口。
在Subject类中,最重要的方法是getName()。该方法返回一个身份名称。在Subject对象中包含了多个Principal对象,因此它可以拥有多个名称。由于登录名称、身份证号和Email地址都可以作为用户的身份标识,可见拥有多个身份名称的情况在实际应用中是非常普遍的情况。
-
Credential 在上面提到的凭证并不是一个特定的类或借口,它可以是任何对象。凭证中可以包含任何特定安全系统需要的验证信息,例如标签(ticket),密钥或口令。Subject对象中维护着一组特定的私有和公有的凭证,这些凭证可以通过getPrivateCredentials()和getPublicCredentials()方法获得。这些方法通常在应用程序层中的安全子系统被调用。
验证 LoginContext,LoginModule,CallBackHandler,Callback
-
LoginContext
在应用程序层中,你可以使用LoginContext对象来验证Subject对象。
LoginContext对象同时体现了JAAS的动态可插入性(Dynamic Pluggability),因为当你创建一个LoginContext的实例时,你需要指定一个配置。LoginContext通常从一个文本文件中加载配置信息,这些配置信息告诉LoginContext对象在登录时使用哪一个LoginModule对象。
下面列出了在LoginContext中经常使用的三个方法:
-
login () 进行登录操作。该方法激活了配置中制定的所有LoginModule对象。如果成功,它将创建一个经过了验证的Subject对象;否则抛出LoginException异常。
-
getSubject () 返回经过验证的Subject对象
-
logout () 注销Subject对象,删除与之相关的Principal对象和凭证
-
-
LoginModule
LoginModule是调用特定验证机制的接口。J2EE 1.4中包含了下面几种LoginModule的实现类:
- JndiLoginModule 用于验证在JNDI中配置的目录服务
- Krb5LoginModule 使用Kerberos协议进行验证
- NTLoginModul 使用当前用户在NT中的用户信息进行验证
- UnixLoginModule 使用当前用户在Unix中的用户信息进行验证
同上面这些模块绑定在一起的还有对应的Principal接口的实现类,例如NTDomainPrincipal和UnixPrincipal。这些类在com.sun.security.auth包中。
LoginModule接口中包含了五个方法:
- initialize () 当创建一LoginModule实例时会被构造函数调用
- login () 进行验证
- commit () 当LgoninContext对象接受所有LoginModule对象传回的结果后将调用该方法。该方法将Principal对象和凭证赋给Subject对象。
- abort () 当任何一个LoginModule对象验证失败时都会调用该方法。此时没有任何Principal对象或凭证关联到Subject对象上。
- logout () 删除与Subject对象关联的Principal对象和凭证。 在应用程序的代码中,程序员通常不会直接调用上面列出的方法,而是通过LigonContext间接调用这些方法。
-
CallbackHandler和Callback
CallbackHandler和Callback对象可以使LoginModule对象从系统和用户那里收集必要的验证信息,同时独立于实际的收集信息时发生的交互过程。 JAAS在javax.sevurity.auth.callback包中包含了七个Callback的实现类和两个CallbackHandler的实现类:ChoiceCallback、ConfirmationCallback、LogcaleCallback、NameCallback、PasswordCallback、TextInputCallback、TextOutputCallback、DialogCallbackHandler和TextCallBackHandler。Callback接口只会在客户端会被使用到。
授权 Policy,AuthPermission,PrivateCredentialPermission
配置文件
JAAS的可扩展性来源于它能够进行动态配置,而配置信息通常是保存在文本。这些文本文件有很多个配置块构成,我们通常把这些配置块称作申请(Application)。每个申请对应了一个或多个特定的LoginModule对象。
当你的代码构造一个LoginContext对象时,你需要把配置文件中申请的名称传递给它。LoginContext将会根据申请中的信息决定激活哪些LoginModule对象,按照什么顺序激活以及使用什么规则激活。
配置文件的结构如下所示:
Application {
ModuleClass Flag ModuleOptions;
ModuleClass Flag ModuleOptions;
...
};
Application {
ModuleClass Flag ModuleOptions;
...
};
...
下面是一个名称为Sample的申请
Sample {
com.sun.security.auth.module.UnixLoginModule Rquired debug=true;
}
上面这个简单的申请指定了LoginContext对象应该使用UnixLoginModule进行验证。类的名称在ModuleClass中被指定。Flag控制当申请中包含了多个LoginModule时进行登录时的行为:Required、Sufficient、Requisite和Optional。最常用的是Required,使用它意味着对应的LoginModule对象必须被调用,并且必须需要通过所有的验证。
ModuleOption允许有多个参数。例如你可以设定调试参数为True(debug=true),这样诊断输出将被送到System.out中。
配置文件可以被任意命名,并且可以被放在任何位置。JAAS框架通过使用java.securty.auth.long.config属性来确定配置文件的位置。
例如当你的应用程序是JaasTest,配置文件是当前目录下的jaas.config,你需要在命令行中输入:
java -Djava.security.auth.login.config=jass.config JavaTest
实现
代码分析
- 程序api使用
LoginContext loginContext = new LoginContext("simple");
loginContext.login();
- 创建一个LoginContext的实例
创建LoginContext是会执行loginContext的init方法,加载配置文件,读取AppConfigurationEntry,放入ModuleStack
private void init(String name) throws LoginException {
...
// get the Configuration
if (config == null) {
config = java.security.AccessController.doPrivileged
(new java.security.PrivilegedAction<Configuration>() {
public Configuration run() {
return Configuration.getConfiguration();//获取配置信息
}
});
}
// get the LoginModules configured for this application
AppConfigurationEntry[] entries = config.getAppConfigurationEntry(name);
if (entries == null) {
if (sm != null && creatorAcc == null) {
sm.checkPermission(new AuthPermission
("createLoginContext." + OTHER));
}
entries = config.getAppConfigurationEntry(OTHER);
if (entries == null) {
MessageFormat form = new MessageFormat(ResourcesMgr.getString
("No.LoginModules.configured.for.name"));
Object[] source = {name};
throw new LoginException(form.format(source));
}
}
moduleStack = new ModuleInfo[entries.length];
for (int i = 0; i < entries.length; i++) {
// clone returned array
moduleStack[i] = new ModuleInfo
(new AppConfigurationEntry
(entries[i].getLoginModuleName(),
entries[i].getControlFlag(),
entries[i].getOptions()),
null);
}
contextClassLoader = java.security.AccessController.doPrivileged
(new java.security.PrivilegedAction<ClassLoader>() {
public ClassLoader run() {
ClassLoader loader =
Thread.currentThread().getContextClassLoader();
if (loader == null) {
// Don't use bootstrap class loader directly to ensure
// proper package access control!
loader = ClassLoader.getSystemClassLoader();
}
return loader;
}
});
}
Configuration.getConfiguration会实例化ConfigFile
public static Configuration getConfiguration() {
...
if (configuration == null) {
String config_class = null;
config_class = AccessController.doPrivileged
(new PrivilegedAction<String>() {
public String run() {
return java.security.Security.getProperty
("login.configuration.provider");
}
});
if (config_class == null) {
config_class = "sun.security.provider.ConfigFile";//config_class默认为ConfigFile,ConfigFile继承自Configuration
}
try {
final String finalClass = config_class;
Configuration untrustedImpl = AccessController.doPrivileged(
new PrivilegedExceptionAction<Configuration>() {
public Configuration run() throws ClassNotFoundException,
InstantiationException,
IllegalAccessException {
Class<? extends Configuration> implClass = Class.forName(
finalClass, false,
Thread.currentThread().getContextClassLoader()
).asSubclass(Configuration.class);
return implClass.newInstance();//实例化ConfigFile
}
});
AccessController.doPrivileged(
new PrivilegedExceptionAction<Void>() {
public Void run() {
setConfiguration(untrustedImpl);//设置configuration=configFile
return null;
}
}, Objects.requireNonNull(untrustedImpl.acc)
);
}
实例化ConfigFile时创建ConfigFile的内部静态类SPI
private final ConfigFile.Spi spi = new ConfigFile.Spi();
public static final class Spi extends ConfigurationSpi {
...
public Spi() {
this.init();
}
private void init() throws IOException {
...
String var5 = System.getProperty("java.security.auth.login.config");//获取java.security.auth.login.config配置文件地址
if (var5 != null) {
boolean var6 = false;
if (var5.startsWith("=")) {
var6 = true;
var5 = var5.substring(1);
}
try {
var5 = PropertyExpander.expand(var5);
} catch (ExpandException var10) {
throw this.ioException("Unable.to.properly.expand.config", var5);
}
URL var7 = null;
try {
var7 = new URL(var5);
} catch (MalformedURLException var12) {
File var9 = new File(var5);
if (!var9.exists()) {
throw this.ioException("extra.config.No.such.file.or.directory.", var5);
}
var7 = var9.toURI().toURL();
}
this.init(var7, var3);//读取配置信息到AppConfigurationEntry对象
var1 = true;
if (var6) {
if (debugConfig != null) {
debugConfig.println("overriding other policies!");
}
this.configuration = var3;
return;
}
...
}
- loginContext执行login验证 login方法两阶段提交:login/commit
public void login() throws LoginException {
loginSucceeded = false;
if (subject == null) {
subject = new Subject();//创建subject对象
}
try {
// module invoked in doPrivileged
invokePriv(LOGIN_METHOD);//执行loginModule的login方法
invokePriv(COMMIT_METHOD);//执行loginModule的init方法
loginSucceeded = true;
} catch (LoginException le) {
try {
invokePriv(ABORT_METHOD);
} catch (LoginException le2) {
throw le;
}
throw le;
}
}
顺序调用配置的loginModule的login和commit方法
private void invoke(String methodName) throws LoginException {
for (int i = moduleIndex; i < moduleStack.length; i++, moduleIndex++) {//遍历moduleStack
...
// 实例化 LoginModule
Class<?> c = Class.forName(
moduleStack[i].entry.getLoginModuleName(),
true,
contextClassLoader);
Constructor<?> constructor = c.getConstructor(PARAMS);
Object[] args = { };
moduleStack[i].module = constructor.newInstance(args);
// 调用LoginModule's initialize 方法
methods = moduleStack[i].module.getClass().getMethods();
for (mIndex = 0; mIndex < methods.length; mIndex++) {
if (methods[mIndex].getName().equals(INIT_METHOD)) {
break;
}
}
Object[] initArgs = {subject,
callbackHandler,
state,
moduleStack[i].entry.getOptions() };
methods[mIndex].invoke(moduleStack[i].module, initArgs);
// 查找 the requested method in the LoginModule and invoke it
for (mIndex = 0; mIndex < methods.length; mIndex++) {
if (methods[mIndex].getName().equals(methodName)) {
break;
}
}
Object[] args = { };
boolean status = ((Boolean)methods[mIndex].invoke
(moduleStack[i].module, args)).booleanValue();