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


75

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

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

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

有人有工作代码示例吗?谢谢。

Answers:


98

可以使用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,因此实际上,这实际上是使用上述其他协议之一的一种round回方式。

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

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

但是,如果认证双方都不具有域帐户,或者如果客户端无法与DC通信,则需要NTLM。因此Kerberos和NTLM不会互斥,并且Kerberos不会淘汰NTLM。实际上,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代码。但是我还没有尝试过。多年来,Sun的Kerberos实施都是一个错误的喜剧,因此,基于Sun在该领域的往绩,在您掌握SPENGO的实现之前,我不会做出任何承诺。

对于NTLM,有一个名为JCIFS的免费OSS项目,该项目具有NTLM HTTP身份验证Servlet过滤器。但是,它使用中间人方法通过与NTLMv2不兼容的SMB服务器(逐渐成为必需的域安全策略)来验证凭据。因此,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服务的通用“安全提供程序”等。

麦克风


5
更新:HttpClient现在支持基本,摘要,NTLMv1,NTLMv2,NTLM2会话,SNPNEGO,Kerberos身份验证方案。
乔纳森·巴贝罗

52

这是我根据此博客中的示例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。
托尔比约恩Ravn的安徒生

6

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

AD(几乎)符合LDAP,我认为您的任务不会有任何问题。我的意思是,它是AD还是任何其他LDAP服务器,只要连接就无所谓。

我来看看:Spring LDAP

他们也有例子。

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

但首先,请确保您可以通过LDAP IDE正确连接到AD。我使用Apache Directory Studio,它真的很酷,并且是用Java编写的。这就是我所需要的。为了进行测试,您还可以安装Apache Directory Server


Luchiani,我目前正在开发Web应用程序以与Java Spring集成以共享点(windows),而我却无法使用Java代码在活动目录中创建用户,您能否在注释中共享在活动目录中创建用户的代码,以便我可以按时继续工作。
Janak Dhanani 2014年

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加密。另一种选择是LDAP over SSL,我的AD服务器不支持。

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

如果您想进行基于Kerberos的用户名/密码身份验证,那么您正在寻找另一条鱼。尽管我不确定它是否执行最后一步来验证身份验证,但它可以与Java GSS-API代码一起使用。(执行验证的代码可以联系AD服务器以检查用户名和密码,这将为用户提供票证授予票证,但要确保未模拟AD服务器,它还需要尝试获取票证以用于用户本身,这会更加复杂。)

如果您要进行基于Kerberos的单点登录,并假设您的用户已通过域验证,则也可以使用Java GSS-API代码来进行此操作。我会发布一个代码示例,但是我仍然需要将我丑陋的原型变成适合人眼的东西。查看SpringSource的一些代码以获取一些启发。

如果您正在寻找NTLM(我得到的理解是不太安全)或其他东西,那么,祝您好运。


非常有用的博客条目。谢谢!
安德烈·罗迪奥诺夫

3

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


是的,仅验证凭据。我明确地编辑了问题。该代码与LDAP身份验证有什么不同吗?
DV。




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链接浏览

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.