Java 安全认证和 Hadoop UGI 原理解析

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 都使用 SubjectCallbackHandler 、共享 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 方法调用。 LoginModulelogin 方法随后执行实际身份验证(例如提示并验证密码)并将其身份验证状态保存为私有状态信息。完成后,LoginModule 的 login 方法要么返回 true(如果成功)或 false(如果应该忽略),要么抛出 LoginException 以指定失败。在失败的情况下,LoginModule 不得重试身份验证或引入延迟。此类任务的责任属于应用程序。如果应用程序尝试重试身份验证,则将再次调用 LoginModule 的 login 方法。

在第二阶段,如果 LoginContext 的整体身份验证成功(相关的 REQUIRED、REQUISITE、SUFFICIENT 和 OPTIONAL LoginModules 成功),则调用 LoginModulecommit 方法。 LoginModulecommit 方法检查其私人保存的状态,以查看其自身的身份验证是否成功。如果整体 LoginContext 身份验证成功并且 LoginModule 自己的身份验证成功,则 commit 方法将相关的主体(经过身份验证的身份)和凭证(身份验证数据,例如加密密钥)与位于 LoginModule 中的 Subject 相关联。

如果 LoginContext 的整体身份验证失败(相关的 REQUIRED、REQUISITE、SUFFICIENT 和 OPTIONAL LoginModule 未成功),则调用每个 LoginModuleabort 方法。在这种情况下,LoginModule 会删除/销毁最初保存的任何身份验证状态。

注销 Subject 仅涉及一个阶段。 LoginContext 调用 LoginModule 的 logout 方法。 LoginModulelogout 方法然后执行注销过程,例如从 Subject 或记录会话信息中删除主体或凭据。

LoginModule 实现必须有一个不带参数的构造函数。这允许加载 LoginModule 的类对其进行实例化。

自定义安全认证案例

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值