Java身份验证和授权服务指南(JAAS)
1. 概述
Java 身份验证和授权服务 (JAAS) 是一个 Java SE 低级安全框架,它将安全模型从基于代码的安全性增强为基于用户的安全性。我们可以将 JAAS 用于两个目的:
- 身份验证:识别当前正在运行代码的实体
- 授权:一旦通过身份验证,确保该实体具有执行敏感代码所需的访问控制权限或权限
在本教程中,我们将介绍如何通过实现和配置其各种 API(尤其是LoginModule)在示例应用程序中设置 JAAS 。
2. JAAS 的工作原理
在应用程序中使用 JAAS 时,涉及到几个 API:
- CallbackHandler:用于收集用户凭据,并在创建LoginContext时可选地提供
- Configuration:负责加载LoginModule实现,可以在创建LoginContext时选择提供
- LoginModule:有效地用于验证用户
我们将使用Configuration API 的默认实现,并为CallbackHandler和 LoginModule API提供我们自己的实现。
3. 提供CallbackHandler实现
在深入研究LoginModule实现之前,我们首先需要为CallbackHandler接口提供一个实现,该接口用于收集用户凭据。
它有一个方法,handle() ,它接受一个Callback数组。此外,JAAS 已经提供了许多Callback实现,我们将分别使用NameCallback和PasswordCallback来收集用户名和密码。
让我们看看我们的CallbackHandler接口的实现:
public class ConsoleCallbackHandler implements CallbackHandler {
@Override
public void handle(Callback[] callbacks) throws UnsupportedCallbackException {
Console console = System.console();
for (Callback callback : callbacks) {
if (callback instanceof NameCallback) {
NameCallback nameCallback = (NameCallback) callback;
nameCallback.setName(console.readLine(nameCallback.getPrompt()));
} else if (callback instanceof PasswordCallback) {
PasswordCallback passwordCallback = (PasswordCallback) callback;
passwordCallback.setPassword(console.readPassword(passwordCallback.getPrompt()));
} else {
throw new UnsupportedCallbackException(callback);
}
}
}
}
因此,为了提示和读取用户名,我们使用了:
NameCallback nameCallback = (NameCallback) callback;
nameCallback.setName(console.readLine(nameCallback.getPrompt()));
同样,提示并读取密码:
PasswordCallback passwordCallback = (PasswordCallback) callback;
passwordCallback.setPassword(console.readPassword(passwordCallback.getPrompt()));
稍后,我们将看到在实现LoginModule时如何调用CallbackHandler。
4. 提供LoginModule实现
为简单起见,我们将提供一个存储硬编码用户的实现。所以,我们称之为InMemoryLoginModule:
public class InMemoryLoginModule implements LoginModule {
private static final String USERNAME = "testuser";
private static final String PASSWORD = "testpassword";
private Subject subject;
private CallbackHandler callbackHandler;
private Map<String, ?> sharedState;
private Map<String, ?> options;
private boolean loginSucceeded = false;
private Principal userPrincipal;
//...
}
在接下来的小节中,我们将给出更重要的方法的实现:initialize()、login()和commit()。
4.1. initialize()
LoginModule首先被加载,然后用Subject 和CallbackHandler初始化。此外,LoginModule可以使用一个Map在它们之间共享数据,另一个Map用于存储私有配置数据:
public void initialize(
Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
this.subject = subject;
this.callbackHandler = callbackHandler;
this.sharedState = sharedState;
this.options = options;
}
4.2. login()
在login()方法中,我们使用NameCallback和PasswordCallback调用*CallbackHandler.handle()*方法来提示并获取用户名和密码。然后,我们将这些提供的凭据与硬编码的凭据进行比较:
@Override
public boolean login() throws LoginException {
NameCallback nameCallback = new NameCallback("username: ");
PasswordCallback passwordCallback = new PasswordCallback("password: ", false);
try {
callbackHandler.handle(new Callback[]{nameCallback, passwordCallback});
String username = nameCallback.getName();
String password = new String(passwordCallback.getPassword());
if (USERNAME.equals(username) && PASSWORD.equals(password)) {
loginSucceeded = true;
}
} catch (IOException | UnsupportedCallbackException e) {
//...
}
return loginSucceeded;
}
login()方法应该为成功的操作返回true ,为失败的登录返回false。**
4.3. commit()
如果对LoginModule#login的所有调用都 成功,我们将使用额外的Principal更新Subject:
@Override
public boolean commit() throws LoginException {
if (!loginSucceeded) {
return false;
}
userPrincipal = new UserPrincipal(username);
subject.getPrincipals().add(userPrincipal);
return true;
}
否则,调用*abort()*方法。
至此,我们的LoginModule实现已准备就绪,需要对其进行配置,以便可以使用Configuration服务提供者动态加载它。
5. LoginModule配置
JAAS 使用配置服务提供者在运行时加载LoginModule。默认情况下,它提供并使用通过登录文件配置LoginModule的ConfigFile实现。例如,这里是用于我们LoginModule的文件的内容:
jaasApplication {
com.blogdemo.jaas.loginmodule.InMemoryLoginModule required debug=true;
};
如我们所见,我们提供了LoginModule实现的完全限定类名、必需标志和调试选项。 最后注意,我们还可以通过java.security.auth.login.config系统属性指定登录文件:
$ java -Djava.security.auth.login.config=src/main/resources/jaas/jaas.login.config
我们还可以通过Java 安全文件*${java.home}/jre/lib/security/java.security中的属性**login.config.url*指定一个或多个登录文件:
login.config.url.1=file:${user.home}/.java.login.config
6. 认证
首先,应用程序通过创建LoginContext 实例来初始化身份验证过程。为此,我们可以查看完整的构造函数,以了解我们需要什么作为参数:
LoginContext(String name, Subject subject, CallbackHandler callbackHandler, Configuration config)
- name:用作仅加载相应LoginModule的索引
- subject:表示要登录的用户或服务
- callbackHandler:负责将用户凭据从应用程序传递到LoginModule
- config : 负责加载name 参数对应的LoginModule
在这里,我们将使用重载的构造函数,我们将在其中提供CallbackHandler实现:
LoginContext(String name, CallbackHandler callbackHandler)
现在我们有了一个CallbackHandler和一个配置的LoginModule,我们可以通过初始化一个LoginContext对象来开始认证过程:
LoginContext loginContext = new LoginContext("jaasApplication", new ConsoleCallbackHandler());
此时,*我们可以调用*login()方法对用户进行身份验证:
loginContext.login();
login()方法反过来创建了我们的LoginModule的一个新实例并调用它的login()方法。而且,**在成功验证后,我们可以检索经过验证的Subject:**
Subject subject = loginContext.getSubject();
现在,让我们运行一个连接了LoginModule的示例应用程序:
$ mvn clean package
$ java -Djava.security.auth.login.config=src/main/resources/jaas/jaas.login.config \
-classpath target/core-java-security-2-0.1.0-SNAPSHOT.jar com.blogdemo.jaas.JaasAuthentication
当系统提示我们提供用户名和密码时,我们将使用testuser和testpassword作为凭据。
7. 授权
当用户第一次连接并与AccessControlContext关联时,授权就会发挥作用。使用 Java 安全策略,我们可以授予Principal一个或多个访问控制权限。然后我们可以通过调用SecurityManager#checkPermission 方法来阻止对敏感代码的访问:
SecurityManager.checkPermission(Permission perm)
7.1. 定义权限
访问控制权限或许可是对资源执行操作的能力。我们可以通过继承Permission抽象类来实现权限。为此,我们需要提供资源名称和一组可能的操作。例如,我们可以使用FilePermission 来配置文件的访问控制权限。可能的操作是read、write、execute等等。对于不需要操作的场景,我们可以简单地使用BasicPermision。
接下来,我们将通过ResourcePermission类提供权限的实现,用户可能有权访问资源:
public final class ResourcePermission extends BasicPermission {
public ResourcePermission(String name) {
super(name);
}
}
稍后,我们将通过 Java 安全策略为此权限配置一个条目。
7.2. 授予权限
通常,我们不需要知道策略文件的语法,因为我们总是可以使用策略工具 来创建一个。让我们看一下我们的策略文件:
grant principal com.sun.security.auth.UserPrincipal testuser {
permission com.blogdemo.jaas.ResourcePermission "test_resource"
};
在此示例中,我们已将test_resource权限授予testuser用户。
7.3. 检查权限
一旦Subject通过身份验证并配置了权限,我们可以通过调用Subject#doAs或Subject#doAsPrivilieged静态方法来检查访问**。为此,我们将提供一个PrivilegedAction,我们可以在其中保护对敏感代码的访问。在run()方法中,我们调用SecurityManager#checkPermission 方法来保证认证用户拥有test_resource权限:
public class ResourceAction implements PrivilegedAction {
@Override
public Object run() {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new ResourcePermission("test_resource"));
}
System.out.println("I have access to test_resource !");
return null;
}
}
最后一件事是调用Subject#doAsPrivileged 方法:
Subject subject = loginContext.getSubject();
PrivilegedAction privilegedAction = new ResourceAction();
Subject.doAsPrivileged(subject, privilegedAction, null);
与身份验证一样,我们将运行一个简单的授权应用程序,除了LoginModule之外,我们还提供了一个权限配置文件:
$ mvn clean package
$ java -Djava.security.manager -Djava.security.policy=src/main/resources/jaas/jaas.policy \
-Djava.security.auth.login.config=src/main/resources/jaas/jaas.login.config \
-classpath target/core-java-security-2-0.1.0-SNAPSHOT.jar com.blogdemo.jaas.JaasAuthorization