Contents

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实现,我们将分别使用NameCallbackPasswordCallback来收集用户名和密码。

让我们看看我们的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()方法中,我们使用NameCallbackPasswordCallback调用*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。默认情况下,它提供并使用通过登录文件配置LoginModuleConfigFile实现。例如,这里是用于我们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

当系统提示我们提供用户名和密码时,我们将使用testusertestpassword作为凭据。

7. 授权

当用户第一次连接并与AccessControlContext关联时,授权就会发挥作用。使用 Java 安全策略,我们可以授予Principal一个或多个访问控制权限。然后我们可以通过调用SecurityManager#checkPermission 方法来阻止对敏感代码的访问:

SecurityManager.checkPermission(Permission perm)

7.1. 定义权限

访问控制权限或许可是对资源执行操作的能力。我们可以通过继承Permission抽象类来实现权限。为此,我们需要提供资源名称和一组可能的操作。例如,我们可以使用FilePermission 来配置文件的访问控制权限。可能的操作是readwriteexecute等等。对于不需要操作的场景,我们可以简单地使用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#doAsSubject#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