1. 概述

Java Authentication and Authorization Service(简称 JAAS)是 Java SE 提供的一套底层安全框架,它的核心价值在于 将 Java 安全模型从“代码级安全”扩展为“用户级安全”。这意味着我们不仅能控制哪些代码能运行,还能控制“谁”可以运行这些代码。

JAAS 主要用于两个关键场景:

认证(Authentication):确认当前执行代码的用户身份
授权(Authorization):在用户通过认证后,检查其是否具备执行敏感操作的权限

本文将通过一个实际示例,带你从零搭建 JAAS 认证授权体系,重点剖析其核心组件,尤其是 LoginModule 的实现与配置。

2. JAAS 核心工作原理

在应用中使用 JAAS,主要涉及以下几个核心接口:

  • CallbackHandler:负责收集用户凭证(如用户名、密码),通常在创建 LoginContext 时传入
  • Configuration:负责加载配置好的 LoginModule 实现,可选地在 LoginContext 创建时指定
  • LoginModule:真正执行用户认证逻辑的模块

本文中,我们将使用 JAAS 默认的 Configuration 实现,而 CallbackHandlerLoginModule 将由我们自定义实现。

3. 实现 CallbackHandler

在深入 LoginModule 之前,必须先实现 CallbackHandler,因为它负责与用户交互,获取登录凭证。

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);
            }
        }
    }
}

⚠️ 注意:System.console() 在某些 IDE 环境下可能为 null,建议在命令行下测试。

4. 实现 LoginModule

为了简化示例,我们实现一个基于内存的 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;
}

接下来我们逐步实现 LoginModule 的关键方法。

4.1 initialize()

LoginModule 在加载后,会先调用 initialize() 方法进行初始化,传入 SubjectCallbackHandler,以及两个用于数据共享和配置的 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() 方法是认证的核心,它通过 CallbackHandler 获取用户输入,并与预设凭证比对:

@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) {
        throw new LoginException("Callback error: " + e.getMessage());
    }
    return loginSucceeded;
}

关键点:认证成功返回 true,失败返回 false。异常情况下抛出 LoginException

4.3 commit()

如果 login() 成功,JAAS 会调用 commit() 方法,将认证成功的用户信息绑定到 Subject 上:

@Override
public boolean commit() throws LoginException {
    if (!loginSucceeded) {
        return false;
    }
    userPrincipal = new UserPrincipal(USERNAME);
    subject.getPrincipals().add(userPrincipal);
    return true;
}

这一步相当于“把用户信息写入会话”。如果认证失败,则调用 abort() 回滚。

5. LoginModule 配置

JAAS 通过 Configuration 服务提供者在运行时动态加载 LoginModule。默认使用基于文件的 ConfigFile 实现。

我们需要创建一个配置文件(如 jaas.login.config),内容如下:

jaasApplication {
   com.baeldung.jaas.loginmodule.InMemoryLoginModule required debug=true;
};

其中:

  • jaasApplication:应用名称,作为 LoginContext 的参数
  • com.baeldung.jaas.loginmodule.InMemoryLoginModuleLoginModule 实现类的全限定名
  • required:表示该模块必须成功(JAAS 支持多种控制标志)
  • debug=true:开启调试日志

启动时通过 JVM 参数指定配置文件位置:

$ java -Djava.security.auth.login.config=src/main/resources/jaas/jaas.login.config

也可在 java.security 文件中通过 login.config.url.1 等属性指定。

6. 执行认证

认证流程从创建 LoginContext 开始。我们使用以下构造函数:

LoginContext(String name, CallbackHandler callbackHandler)

参数说明:

  • name:对应配置文件中的应用名(如 jaasApplication
  • callbackHandler:我们实现的凭证收集器

开始认证:

LoginContext loginContext = new LoginContext("jaasApplication", new ConsoleCallbackHandler());
loginContext.login(); // 触发认证流程

login() 方法会自动加载配置的 LoginModule 并调用其 login() 方法。

认证成功后,可通过以下方式获取用户主体:

Subject subject = loginContext.getSubject();

完整启动命令示例:

$ 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.baeldung.jaas.JaasAuthentication

输入用户名 testuser 和密码 testpassword 即可通过认证。

7. 执行授权

认证通过后,JAAS 会将 Subject 与当前的 AccessControlContext 关联。接下来就可以基于策略进行授权控制。

7.1 定义权限

权限(Permission)代表对某个资源执行某种操作的能力。我们可以继承 BasicPermission 定义自定义权限:

public final class ResourcePermission extends BasicPermission {
    public ResourcePermission(String name) {
        super(name);
    }
}

这里定义了一个名为 test_resource 的资源权限。

7.2 授予权限

通过 Java 安全策略文件(policy file)为特定用户授予权限。策略文件内容如下:

grant principal com.sun.security.auth.UserPrincipal testuser {
    permission com.baeldung.jaas.ResourcePermission "test_resource";
};

✅ 这行配置表示:授予 testuser 用户对 test_resource 的访问权限。

7.3 检查权限

在敏感代码执行前,使用 Subject.doAsPrivileged() 并调用 SecurityManager.checkPermission() 进行权限校验:

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 subject = loginContext.getSubject();
PrivilegedAction privilegedAction = new ResourceAction();
Subject.doAsPrivileged(subject, privilegedAction, null);

完整启动命令(启用安全管理器):

$ 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.baeldung.jaas.JaasAuthorization

如果用户没有权限,会抛出 AccessControlException

8. 总结

本文通过一个完整示例,演示了 JAAS 的核心组件实现与配置流程:

  • ✅ 自定义 CallbackHandler 收集用户凭证
  • ✅ 实现 LoginModule 完成认证逻辑
  • ✅ 配置 JAAS 登录模块与安全策略
  • ✅ 结合 Subject.doAsPrivileged() 实现细粒度授权

JAAS 虽然学习曲线较陡,但其模块化设计非常适合需要高安全级别的企业级应用。踩坑提示:务必在真实环境中测试 System.console() 的可用性,并合理使用 debug 模式排查配置问题。

文中所有代码已托管至 GitHub:

https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-security-2


原始标题:Guide to the Java Authentication And Authorization Service (JAAS)