76

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

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

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

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

4

9 回答 9

98

有 3 种身份验证协议可用于在 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。从理论上讲,您应该能够编写一些可以验证 IE 客户端的 GSSAPI 代码。但我还没有尝试过。多年来,Kerberos 的 Sun 实施一直是错误的喜剧,因此根据 Sun 在该领域的记录,我不会对他们的 SPENGO 实施做出任何承诺,除非您掌握了那只鸟。

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

使用 Active Directory 验证 NTLM 凭据的唯一正确方法是通过具有安全通道的 NETLOGON 使用 NetrLogonSamLogon DCERPC 调用。Java中是否存在这样的事情?是的。这里是:

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

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

麦克风

于 2009-01-17T18:14:13.943 回答
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();
    }

}
于 2008-12-26T19:51:44.513 回答
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

于 2008-12-23T23:55:38.300 回答
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 允许 LDAP 绑定到“user@domain”而不使用帐户的专有名称。此代码使用 StartTLS 在连接上启用 TLS 加密;另一种选择是基于 SSL 的 LDAP,我的AD 服务器不支持。

真正的诀窍在于定位服务器和主机。官方方法是使用 DNS SRV(服务)记录查找来定位一组候选主机,然后执行基于 UDP 的 LDAP“ping”(以特定的 Microsoft 格式)来定位正确的服务器。如果您有兴趣,我已经发布了一些关于我在该地区的冒险和发现之旅的博客文章。

如果您想进行基于 Kerberos 的用户名/密码验证,那么您正在寻找另一锅鱼;它可以使用 Java GSS-API 代码,尽管我不确定它是否执行最后一步来验证身份验证。(做验证的代码可以联系AD服务器检查用户名和密码,从而为用户提供ticket授予ticket,但要确保AD服务器没有被冒充,还需要尝试获取ticket用户自己,这有点复杂。)

如果您想要执行基于 Kerberos 的单点登录,假设您的用户已通过域的身份验证,您也可以使用 Java GSS-API 代码执行此操作。我会发布一个代码示例,但我仍然需要将我可怕的原型变成适合人眼的东西。查看SpringSource 中的一些代码以获得一些灵感。

如果您正在寻找 NTLM(我被告知不太安全)或其他东西,那么,祝您好运。

于 2009-11-20T19:56:12.803 回答
3

您只是在验证凭据吗?在这种情况下,您可以简单地做kerberos而不用打扰LDAP.

于 2008-12-23T21:49:31.347 回答
2

如果您只想使用 Kerberos 对 AD 进行身份验证,那么一个简单的http://spnego.sourceforge.net/HelloKDC.java程序就可以完成。

查看该项目的“飞行前”文档,其中讨论了 HelloKDC.java 程序。

于 2009-11-04T16:10:22.657 回答
1

http://java.sun.com/docs/books/tutorial/jndi/ldap/auth_mechs.html

SASL 机制支持 Kerberos v4 和 v5。 http://java.sun.com/docs/books/tutorial/jndi/ldap/sasl.html

于 2008-12-23T22:17:29.293 回答
1

没有 SSL 的 ldap 身份验证是不安全的,任何人都可以查看用户凭据,因为 ldap 客户端在 ldap 绑定操作期间会传输用户名和密码,因此请始终使用 ldaps 协议。来源: Java Spring Security 中的 Ldap 身份验证 Active Directory 示例

于 2011-11-19T16:35:42.913 回答
0

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

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

于 2012-06-18T19:06:02.013 回答