Java 安全认证和 UGI 原理解析
一般来说,Java 安全认证主要通过自定义 Subject、LoginContext、LoginModule、Configuration 在 Java 中进行安全认证。
Subject 可以单独创建并通过 Subject#doAs 方法单独进行登录,但也可以传入 LoginContext 中,等待 LoginContext 登录完将登录后的 敏感凭证存入 Subject 后再进行登录。
LoginContext 定义了登录环境,环境的创建需要三个必要的条件:环境名,Subject、Configuration。
环境名一般用于指定认证的环境,如通过环境名获取简单认证还是安全认证。
Subject 用于在创建并登录 LoginContext 过程中进行认证,认证过程中可以进行一些操作,例如认证完后将一些敏感凭据存入 Subject中,然后 Subject 作为后面登录和使用的主体。
Configuration 用于定义登录环境登录时将进行认证的若干 LoginModule,以及 LoginModule 需要的参数,例如官方的 Krb5LoginModule 模块,以及其相关参数:principal、KeyTab、ticketCache 等等,具体可以查看 Java 源码或者文档。
Java 有若干个 LoginModule 的实现类,如下:
- JndiLoginModule: 该模块提示输入用户名和密码,然后根据 JNDI 下配置的目录服务中存储的密码验证密码。
- KeyStoreLoginModule: 提供 JAAS 登录模块,提示输入密钥存储别名并使用别名的主体和凭据填充主题。为主体主体中别名凭据中第一个证书的主体可分辨名称存储X500Principal,在主体公共凭据中存储别名证书路径,以及X500PrivateCredential,其证书是别名证书路径中的第一个证书,其私钥是主题私人凭证中的别名私钥。
- Krb5LoginModule: 此 LoginModule 使用 Kerberos 协议对用户进行身份验证。
- LdapLoginModule: 此 LoginModule 执行基于 LDAP 的身份验证。根据存储在 LDAP 目录中的相应用户凭证验证用户名和密码。该模块需要提供的 CallbackHandler 来支持 NameCallback 和 PasswordCallback 。如果身份验证成功,则使用用户的可分辨名称创建一个新的LdapPrincipal ,并使用用户的用户名创建一个新的UserPrincipal ,并且两者都与当前的Subject 相关联。
- NTLoginModule: 此 LoginModule 将用户的 NT 安全信息呈现为一定数量的 Principal 并将它们与 Subject 相关联。
- UnixLoginModule: 此 LoginModule 导入用户的 Unix Principal 信息(UnixPrincipal、UnixNumericUserPrincipal 和 UnixNumericGroupPrincipal)并将它们与当前的 Subject 关联。
模块具体的原理以及相关的 Configuration 可以通过查看 Java 官方文档,Javadocs、或者Java 源码。
它们都可以拿来即用,不用我们再一次进行封装。
LoginContext 登录完后,我们再通过 Subject#doAs 方法登录并进行相关操作,该方法为静态方法,需要传入Subject 和 PrivilegedAction, PrivilegedAction 为一个回调方法,也就是在认证完之后进行的一个回调方法。
PrivilegedAction 相当于一个域,可以随处在当前函数里面获取 Subject#doAs 时传入的 Subject。
AccessControlContext context = ;
return Subject.getSubject(context);
这种域中获取 Subject 的操作经常用于一些框架和服务端进行安全认证,常见与 Hadoop 的 UserInformation ,Hadoop 许多客户端在与服务端通信时都会从域中获取 Subject ,然后使用 Subject 里面的安全凭据与服务端通信。
Subject 和 LoginModule 官方解析
Subject 官方描述
Subject
表示单个实体(例如人)的一组相关信息。此类信息包括主体的身份及其与安全相关的属性(例如,密码和加密密钥)。
受试者可能具有多重身份。每个身份都表示为 Subject
中的 Principal
。校长只需将名称绑定到 Subject
即可。例如,恰好是一个人 Alice 的 Subject
可能有两个委托人:一个将她驾驶执照上的名字"Alice Bar"绑定到 Subject
,另一个绑定"999-99-9999" ,她的学生证上的号码,到Subject
。两个 Principal 都引用相同的 Subject
,尽管每个 Principal 的名称不同。
Subject
也可能拥有与安全相关的属性,这些属性被称为凭证。需要特殊保护的敏感凭据(例如私人加密密钥)存储在私人凭据 Set
中。用于共享的凭据(例如公钥证书或 Kerberos 服务票证)存储在公共凭据 Set
中。访问和修改不同的凭证集需要不同的权限。
要检索与 Subject
关联的所有主体,请调用 getPrincipals
方法。要检索属于 Subject
的所有公共或私有凭证,请分别调用 getPublicCredentials
方法或 getPrivateCredentials
方法。要修改返回的 Set
of Principals 和凭据,请使用 Set
类中定义的方法。例如:
Subject
类实现了 Serializable
。虽然与 Subject
关联的主体被序列化,但与 Subject
关联的凭据没有。请注意,java.security.Principal
类未实现 Serializable
。因此,与 Subjects 关联的所有具体 Principal
实现都必须实现 Serializable
。
LoginModule 官方描述
身份验证技术提供商的服务提供者接口。 LoginModules 在应用程序下插入以提供特定类型的身份验证。
当应用程序写入 LoginContext
API 时,身份验证技术提供商会实现 LoginModule
接口。 Configuration
指定要与特定登录应用程序一起使用的登录模块。因此,可以在应用程序下插入不同的登录模块,而无需对应用程序本身进行任何修改。
LoginContext
负责读取 Configuration
并实例化适当的登录模块。每个 LoginModule
都使用 Subject
、 CallbackHandler
、共享 LoginModule
状态和特定于 LoginModule 的选项进行初始化。
Subject
表示当前正在验证的 Subject
,如果验证成功,则会使用相关凭证进行更新。 LoginModules 使用CallbackHandler
与用户进行通信。例如,CallbackHandler
可用于提示输入用户名和密码。请注意,CallbackHandler
可能是 null
。绝对需要 CallbackHandler
来验证 Subject
的登录模块可能会抛出 LoginException
。 LoginModule 可选择使用共享状态在它们之间共享信息或数据。
LoginModule 特定的选项表示管理员或用户在登录 Configuration
中为此 LoginModule
配置的选项。选项由 LoginModule
本身定义并控制其中的行为。例如,LoginModule
可以定义支持调试/测试功能的选项。选项使用键值语法定义,例如调试=真 LoginModule
将选项存储为 Map
,以便可以使用密钥检索值。请注意,LoginModule
选择定义的选项数量没有限制。
调用应用程序将身份验证过程视为单个操作。然而,LoginModule
中的身份验证过程分两个不同的阶段进行。在第一阶段,LoginModule 的 login
方法被 LoginContext 的 login
方法调用。 LoginModule
的 login
方法随后执行实际身份验证(例如提示并验证密码)并将其身份验证状态保存为私有状态信息。完成后,LoginModule 的 login
方法要么返回 true
(如果成功)或 false
(如果应该忽略),要么抛出 LoginException
以指定失败。在失败的情况下,LoginModule
不得重试身份验证或引入延迟。此类任务的责任属于应用程序。如果应用程序尝试重试身份验证,则将再次调用 LoginModule 的 login
方法。
在第二阶段,如果 LoginContext 的整体身份验证成功(相关的 REQUIRED、REQUISITE、SUFFICIENT 和 OPTIONAL LoginModules 成功),则调用 LoginModule
的 commit
方法。 LoginModule
的 commit
方法检查其私人保存的状态,以查看其自身的身份验证是否成功。如果整体 LoginContext
身份验证成功并且 LoginModule 自己的身份验证成功,则 commit
方法将相关的主体(经过身份验证的身份)和凭证(身份验证数据,例如加密密钥)与位于 LoginModule
中的 Subject
相关联。
如果 LoginContext 的整体身份验证失败(相关的 REQUIRED、REQUISITE、SUFFICIENT 和 OPTIONAL LoginModule 未成功),则调用每个 LoginModule
的 abort
方法。在这种情况下,LoginModule
会删除/销毁最初保存的任何身份验证状态。
注销 Subject
仅涉及一个阶段。 LoginContext
调用 LoginModule 的 logout
方法。 LoginModule
的 logout
方法然后执行注销过程,例如从 Subject
或记录会话信息中删除主体或凭据。
LoginModule
实现必须有一个不带参数的构造函数。这允许加载 LoginModule
的类对其进行实例化。
自定义安全认证案例
java
public class TestLoginModule {
@Test
void testSuccess() throws LoginException {
Subject subject = new Subject();
Set<Object> privateCredentials = subject.getPrivateCredentials();
privateCredentials.add(new UserPrincipal("admin", "123456"));
LoginContext loginContext = new LoginContext(
"custom",
subject,
null,
new CustomConfiguration());
loginContext.login();
Subject.doAs(subject, (PrivilegedAction<?>) () -> {
CustomServer.doSomething();
return null;
});
}
@Test
void testLoginModuleAuthenticateFailed() throws LoginException {
Subject subject = new Subject();
Set<Object> privateCredentials = subject.getPrivateCredentials();
privateCredentials.add(new UserPrincipal("hello", "123456"));
LoginContext loginContext = new LoginContext(
"custom",
subject,
null,
new CustomConfiguration());
Assertions.assertThrows(Exception.class, loginContext::login);
}
@Test
void testServerAuthenticateFailed() {
Subject subject = new Subject();
Set<Object> privateCredentials = subject.getPrivateCredentials();
privateCredentials.add(new UserPrincipal("hello", "123456"));
Assertions.assertThrows(Exception.class, () ->
Subject.doAs(subject, (PrivilegedAction<?>)
() -> {
CustomServer.doSomething();
return null;
}));
}
public static class CustomServer {
private static void doSomething() {
Subject currentSubject = getCurrentSubject();
Set<UserTicket> privateCredentials = currentSubject.getPrivateCredentials(UserTicket.class);
for (UserTicket privateCredential : privateCredentials) {
if (SecurityCenter.authenticate(privateCredential.getCredit())) {
System.out.println("Doing something");
return;
}
}
throw new RuntimeException("Authenticate failed");
}
}
private static Subject getCurrentSubject() {
AccessControlContext context = AccessController.getContext();
return Subject.getSubject(context);
}
public static class CustomConfiguration extends javax.security.auth.login.Configuration {
// 模块认证时的参数
private static final Map<String, String> BASIC_OPTIONS =
new HashMap<>();
// 自定义认证模块
private static final AppConfigurationEntry LOGIN =
new AppConfigurationEntry(
CustomLoginModule.class.getName(),
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
BASIC_OPTIONS);
// 需要认证的模块
private static final AppConfigurationEntry[] SIMPLE_CONF =
new AppConfigurationEntry[]{LOGIN};
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
return SIMPLE_CONF;
}
}
public static class CustomLoginModule implements LoginModule {
private Subject subject;
private String credit;
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
this.subject = subject;
System.out.println(subject);
}
@Override
public boolean login() throws LoginException {
Set<UserPrincipal> principals = subject.getPrivateCredentials(UserPrincipal.class);
for (UserPrincipal principal : principals) {
String signature = SecurityCenter.login(principal.getName(), principal.getPassword());
if (signature != null) {
credit = signature;
return true;
}
}
return false;
}
@Override
public boolean commit() throws LoginException {
Set<Object> privateCredentials = subject.getPrivateCredentials();
UserTicket userTicket = new UserTicket(credit);
privateCredentials.add(userTicket);
return true;
}
public boolean abort() throws LoginException {
System.out.println("abort");
return false;
}
@Override
public boolean logout() throws LoginException {
System.out.println("logout");
return true;
}
}
public static class UserPrincipal implements Principal {
private final String name;
private final String password;
UserPrincipal(String name, String password) {
this.name = name;
this.password = password;
}
public String getPassword() {
return password;
}
@Override
public String getName() {
return name;
}
@Override
public String toString() {
return "UserPrincipal{" +
"name='" + name + '\'' +
", password='" + password + '\'' +
'}';
}
}
public static class UserTicket implements Destroyable, Refreshable {
private final String credit;
public UserTicket(String credit) {
this.credit = credit;
}
public String getCredit() {
return credit;
}
@Override
public boolean isCurrent() {
return false;
}
@Override
public void refresh() throws RefreshFailedException {
}
}
public static class SecurityCenter {
public static Set<String> cache = new HashSet<>();
private static String login(String username, String password) {
if (username.equals("admin") && "123456".equals(password)) {
String uuid = UUID.randomUUID().toString();
cache.add(uuid);
return uuid;
}
return null;
}
private static boolean authenticate(String credit) {
return cache.contains(credit);
}
}
}
参考:
-
Java LoginModule 类官方文档: https://doc.qzxdp.cn/jdk/20/zh/api/java.base/javax/security/auth/spi/LoginModule.html
-
Java Subject 类官方文档: https://doc.qzxdp.cn/jdk/20/zh/api/java.base/javax/security/auth/Subject.html
-
Hadoop UserGroupInformation 类官方文档: https://hadoop.apache.org/docs/stable/api/org/apache/hadoop/security/UserGroupInformation.html