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

76

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

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

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

有没有人有可行代码的示例?谢谢。

9个回答

99

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

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

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

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

还有一种称为“ADFS”的东西,它使用调用Windows SSP的SAML为网站提供SSO,因此实际上是使用上述其他协议的迂回方式。

每个协议都有其优点,但通常应尽量“按照Windows的做法”以实现最大兼容性。那么Windows怎么做呢?

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

但是,如果验证方不都具有域帐户,或者如果客户端无法与DC通信,则需要NTLM。因此,Kerberos和NTLM并不是相互排斥的,而且NTLM并没有被Kerberos淘汰。实际上,在某些方面,NTLM比Kerberos更好。请注意,当在同一句话中提到Kerberos和NTLM时,我还必须提到SPENGO和集成Windows身份验证(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已经得到实现。理论上,您应该能够编写一些GSSAPI代码来验证IE客户端。但我没有尝试过。多年来,Sun的Kerberos实现一直是一个笑话,因此基于Sun在这个领域的记录,我不会对他们的SPENGO实现做出任何承诺,直到您掌握了这个技术。

对于NTLM,有一个名为JCIFS的免费OSS项目,它具有NTLM HTTP身份验证Servlet过滤器。但是,它使用中间人方法来验证凭据与不支持NTLMv2(这正在逐渐成为必需的域安全策略)的SMB服务器。因此,JCIFS的HTTP过滤器部分将被删除。请注意,有许多衍生项目使用JCIFS来实现相同的技术。因此,如果您看到其他声称支持NTLM SSO的项目,请查看详细信息。
使用NetrLogonSamLogon DCERPC调用通过NETLOGON和Secure Channel验证NTLM凭据与Active Directory的唯一正确方法。Java中是否存在这样的东西?是的。在这里:

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

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

5
更新:HttpClient 现在支持基本认证、摘要认证、NTLMv1、NTLMv2、NTLM2 会话、SNPNEGO 和 Kerberos 认证方案。 - Jonathan Barbero

53

这是我根据博客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();
    }

}

4
import com.sun.jndi.ldap.LdapCtxFactory; - 这很可能只适用于Sun JVM。 - Thorbjørn Ravn Andersen

6

我刚刚完成了一个使用AD和Java的项目。

我们使用了Spring ldapTemplate。

AD兼容LDAP(几乎),我认为您不会在任务中遇到任何问题。我的意思是,如果您只想连接,那么它是AD或任何其他LDAP服务器都无关紧要。

我建议看一下:Spring LDAP

他们也有示例。

至于加密,我们使用了SSL连接(因此它是LDAPS)。 AD必须在SSL端口/协议上进行配置。

但首先,请确保您可以通过LDAP IDE正确连接到您的AD。 我使用Apache Directory Studio,它非常酷,而且它是用Java编写的。这就是我需要的全部。 为了测试目的,您还可以安装Apache Directory Server


Luchiani,我目前正在开发一个将Java Spring集成到SharePoint(Windows)的Web应用程序。但是我无法使用Java代码在Active Directory中创建用户,请问你能否分享一下你的带注释的创建Active Directory用户的代码,这样我就可以按时继续我的工作了。 - Janak Dhanani

5

正如ioplex和其他人所说,有很多选择。要使用LDAP(以及Novell LDAP API)进行身份验证,我通常会使用类似以下的代码:


LDAPConnection connection = new LDAPConnection( new LDAPJSSEStartTLSFactory() );
connection.connect(hostname, port);
connection.startTLS();
connection.bind(LDAPConnection.LDAP_V3, username+"@"+domain, password.getBytes());

作为“特殊功能”,Active Directory允许使用“user@domain”进行LDAP绑定,而无需使用帐户的可分辨名称。此代码使用StartTLS在连接上启用TLS加密;另一种选择是LDAP over SSL,但我的AD服务器不支持。
真正的诀窍在于定位服务器和主机;官方方法是使用DNS SRV(服务)记录查找以定位一组候选主机,然后执行基于UDP的LDAP“ping”(以特定的Microsoft格式)以定位正确的服务器。如果您有兴趣,我已发布了一些博客文章,介绍了我在该领域中的冒险和发现之旅。
如果您想进行基于Kerberos的用户名/密码身份验证,则需要考虑另一种情况;使用Java GSS-API代码可以实现,尽管我不确定它是否执行最终验证步骤。(执行验证的代码可以联系AD服务器以检查用户名和密码,这会为用户生成票证授权票据,但为了确保未被仿冒AD服务器,还需要尝试向自己获取用户的票证,这更加复杂。)
如果您想进行基于Kerberos的单点登录,假设您的用户已通过域进行身份验证,则可以使用Java GSS-API代码实现。我会发布一些代码示例,但我仍需要将我的丑陋原型转化为适合人类阅读的东西。请查看SpringSource的一些代码以获取灵感。
如果您正在寻找NTLM(据我所知不太安全)或其他内容,祝你好运。

非常有用的博客文章。谢谢! - dernasherbrezon

3

您只是在验证凭据吗?如果是这样,您可以使用普通的kerberos而不必使用LDAP


是的,只是验证凭据。我已经编辑了问题并进行了澄清。这段代码与LDAP身份验证有什么不同吗? - DV.

2
如果您只想使用Kerberos对AD进行身份验证,那么一个简单的http://spnego.sourceforge.net/HelloKDC.java程序就可以实现。请查看该项目的“预检”文档,其中讨论了HelloKDC.java程序。


1

0

我建议您查看oVirt项目的adbroker包。它使用Spring-Ldap和Krb5 JAAS登录模块(带有GSSAPI)来针对Ldap服务器(Active-Directory、ipa、rhds、Tivoli-DS)进行Kerberos身份验证。请在engine\backend\manager\modules\bll\src\main\java\org\ovirt\engine\core\bll\adbroker中查找代码。

您可以使用git克隆存储库,或使用gerrit链接浏览。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接