在 Linux 上使用 Java 对 Active Directory 进行身份验证

我有一个简单的任务,使用Java对Active Directory进行身份验证。只是验证凭据,没有别的。假设我的域是“fun.xyz.tld”,OU路径未知,用户名/密码是testu/testp。

我知道有一些Java库可以简化这项任务,但我并没有成功地实现它们。我发现的大多数示例通常都针对LDAP,而不是特定的Active Directory。发出 LDAP 请求意味着在其中发送一个 OU 路径,而我没有。此外,发出LDAP请求的应用程序应该已经绑定到Active Directory才能访问它...不安全,因为凭据必须存储在可发现的地方。如果可能的话,我想要一个带有测试凭据的测试绑定 - 这意味着该帐户是有效的。

最后,如果可能的话,有没有办法使这种身份验证机制加密?我知道AD使用Kerberos,但不确定Java的LDAP方法是否使用。

有没有人有工作代码的例子?谢谢。


答案 1

有3种身份验证协议可用于在Linux或任何其他平台上的Java和Active Directory之间执行身份验证(这些协议不仅特定于HTTP服务):

  1. Kerberos - Kerberos 提供单点登录 (SSO) 和委派,但 Web 服务器也需要 SPNEGO 支持才能通过 IE 接受 SSO。

  2. NTLM - NTLM 通过 IE(以及其他浏览器,如果配置正确)支持 SSO。

  3. LDAP - LDAP 绑定可用于简单地验证帐户名和密码。

还有一种叫做“ADFS”的东西,它为使用SAML的网站提供SSO,这些网站调用Windows SSP,所以在实践中,它基本上是使用上述其他协议之一的迂回方式。

每个协议都有其优点,但根据经验,为了获得最大的兼容性,您通常应该尝试“像Windows一样做”。那么Windows是做什么的呢?

首先,两台 Windows 计算机之间的身份验证有利于 Kerberos,因为服务器不需要与 DC 通信,并且客户端可以缓存 Kerberos 票证,从而减少 DC 上的负载(并且因为 Kerberos 支持委派)。

但是,如果身份验证方没有域帐户,或者客户端无法与 DC 通信,则需要 NTLM。因此,Kerberos 和 NTLM 并不相互排斥,NTLM 也没有被 Kerberos 淘汰。事实上,在某些方面,NTLM 比 Kerberos 更好。请注意,当同时提到Kerberos和NTLM时,我还必须提到SPENGO和Integrated Windows Authentication(IWA)。IWA是一个简单的术语,基本上意味着Kerberos或NTLM或SPNEGO来协商Kerberos或NTLM。

使用 LDAP 绑定作为验证凭据的方法效率不高,并且需要 SSL。但直到最近,实现 Kerberos 和 NTLM 一直很困难,因此使用 LDAP 作为临时身份验证服务一直存在。但在这一点上,通常应该避免。LDAP 是一个信息目录,而不是身份验证服务。将其用于预期目的。

那么,如何在Java中,特别是在Web应用程序的上下文中实现Kerberos或NTLM呢?

有许多大公司,如Quest Software和Centrify,都有专门提到Java的解决方案。我不能真正评论这些,因为它们是公司范围的“身份管理解决方案”,所以,从他们网站上的营销旋转来看,很难确切地说出正在使用哪些协议以及如何使用。您需要与他们联系以获取详细信息。

在Java中实现Kerberos并不是非常困难,因为标准Java库通过org.ietf.gssapi类支持Kerberos。但是,直到最近,还存在一个主要障碍 - IE不发送原始的Kerberos令牌,而是发送SPNEGO令牌。但是在Java 6中,SPNEGO已经实现。从理论上讲,您应该能够编写一些可以对IE客户端进行身份验证的GSSAPI代码。但我还没有尝试过。多年来,Kerberos的Sun实现一直是一部错误的喜剧,因此基于Sun在这一领域的记录,在你拥有这只鸟之前,我不会对他们的SPENGO实现做出任何承诺。

对于NTLM,有一个名为JCIFS的免费OSS项目,它具有NTLM HTTP身份验证Servlet过滤器。但是,它使用中间人方法通过不支持 NTLMv2(正在慢慢成为必需的域安全策略)的 SMB 服务器来验证凭据。出于这个原因和其他原因,JCIFS的HTTP过滤器部分被计划被删除。请注意,有许多使用 JCIFS 实现相同技术的分拆。因此,如果您看到其他声称支持 NTLM SSO 的项目,请查看细则。

使用Active Directory验证NTLM凭据的唯一正确方法是使用NetrLogonSamLogon DCERPC调用NETLOGON与Secure Channel。Java中存在这样的东西吗?是的。在这里:

http://www.ioplex.com/jespa.html

Jespa是一个100%的Java NTLM实现,支持NTLMv2,NTLMv1,完全完整性和机密性选项以及前面提到的NETLOGON凭据验证。它包括一个HTTP SSO过滤器,一个JAAS LoginModule,HTTP客户端,SASL客户端和服务器(带有JNDI绑定),用于创建自定义NTLM服务的通用“安全提供程序”等等。

话筒


答案 2

以下是我根据此博客中的示例编写的代码:LINK和此来源:LINK

import com.sun.jndi.ldap.LdapCtxFactory;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Iterator;
import javax.naming.Context;
import javax.naming.AuthenticationException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import static javax.naming.directory.SearchControls.SUBTREE_SCOPE;

class App2 {

    public static void main(String[] args) {

        if (args.length != 4 && args.length != 2) {
            System.out.println("Purpose: authenticate user against Active Directory and list group membership.");
            System.out.println("Usage: App2 <username> <password> <domain> <server>");
            System.out.println("Short usage: App2 <username> <password>");
            System.out.println("(short usage assumes 'xyz.tld' as domain and 'abc' as server)");
            System.exit(1);
        }

        String domainName;
        String serverName;

        if (args.length == 4) {
            domainName = args[2];
            serverName = args[3];
        } else {
            domainName = "xyz.tld";
            serverName = "abc";
        }

        String username = args[0];
        String password = args[1];

        System.out
                .println("Authenticating " + username + "@" + domainName + " through " + serverName + "." + domainName);

        // bind by using the specified username/password
        Hashtable props = new Hashtable();
        String principalName = username + "@" + domainName;
        props.put(Context.SECURITY_PRINCIPAL, principalName);
        props.put(Context.SECURITY_CREDENTIALS, password);
        DirContext context;

        try {
            context = LdapCtxFactory.getLdapCtxInstance("ldap://" + serverName + "." + domainName + '/', props);
            System.out.println("Authentication succeeded!");

            // locate this user's record
            SearchControls controls = new SearchControls();
            controls.setSearchScope(SUBTREE_SCOPE);
            NamingEnumeration<SearchResult> renum = context.search(toDC(domainName),
                    "(& (userPrincipalName=" + principalName + ")(objectClass=user))", controls);
            if (!renum.hasMore()) {
                System.out.println("Cannot locate user information for " + username);
                System.exit(1);
            }
            SearchResult result = renum.next();

            List<String> groups = new ArrayList<String>();
            Attribute memberOf = result.getAttributes().get("memberOf");
            if (memberOf != null) {// null if this user belongs to no group at all
                for (int i = 0; i < memberOf.size(); i++) {
                    Attributes atts = context.getAttributes(memberOf.get(i).toString(), new String[] { "CN" });
                    Attribute att = atts.get("CN");
                    groups.add(att.get().toString());
                }
            }

            context.close();

            System.out.println();
            System.out.println("User belongs to: ");
            Iterator ig = groups.iterator();
            while (ig.hasNext()) {
                System.out.println("   " + ig.next());
            }

        } catch (AuthenticationException a) {
            System.out.println("Authentication failed: " + a);
            System.exit(1);
        } catch (NamingException e) {
            System.out.println("Failed to bind to LDAP / get account information: " + e);
            System.exit(1);
        }
    }

    private static String toDC(String domainName) {
        StringBuilder buf = new StringBuilder();
        for (String token : domainName.split("\\.")) {
            if (token.length() == 0)
                continue; // defensive check
            if (buf.length() > 0)
                buf.append(",");
            buf.append("DC=").append(token);
        }
        return buf.toString();
    }

}

推荐