Java身份验证和授权服务(JAAS)指南

2023/07/02

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已经提供了许多回调实现,我们将分别使用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实现已准备就绪,需要对其进行配置,以便使用配置服务提供程序动态加载它。

5. LoginModule配置

JAAS使用配置服务提供程序在运行时加载LoginModule,默认情况下,它提供并使用ConfigFile实现,其中LoginModule通过登录文件进行配置。例如,这是用于我们的LoginModule的文件的内容:

jaasApplication {
   cn.tuyucheng.taketoday.jaas.loginmodule.InMemoryLoginModule required debug=true;
};

如我们所见,我们提供了LoginModule实现的完全限定类名、一个required标志和一个调试选项。

最后请注意,我们还可以通过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/java-security-2-1.0.0.jar cn.tuyucheng.taketoday.jaas.JaasAuthentication

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

7. 授权

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

SecurityManager.checkPermission(Permission perm)

7.1 定义权限

访问控制权限或许可是对资源执行操作的能力,我们可以通过继承Permission抽象类来实现权限。为此,我们需要提供资源名称和一组可能的操作。例如,我们可以使用FilePermission来配置对文件的访问控制权限,可能的操作有read、write、execute等等。对于不需要操作的场景,我们可以简单地使用BasicPermission。

接下来,我们将通过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 cn.tuyucheng.taketoday.jaas.ResourcePermission "test_resource"
};

在此示例中,我们已将test_resource权限授予testuser用户

7.3 检查权限

一旦对Subject进行了身份验证并配置了权限,我们就可以通过调用Subject#doAs或Subject#doAsPrivileged静态方法来检查访问权限。为此,我们将提供一个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/java-security-2-1.0.0.jar cn.tuyucheng.taketoday.jaas.JaasAuthorization

8. 总结

在本文中,我们通过探索主要类和接口并演示如何配置它们,展示了如何实现JAAS。特别是,我们实现了服务提供程序LoginModule。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章