Java LDAP - 如何确定用户是否在给定的组中?

29

我们使用Java LDAP API来登录Active Directory。现在我们想要增强登录功能,以进一步检查用户是否在给定的AD组中。有人知道如何实现吗?

当前代码:

import javax.naming.*;
import javax.naming.ldap.*;

LdapContext ctx = null;
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.SECURITY_AUTHENTICATION,"simple");
env.put(Context.PROVIDER_URL, Config.get("ldap-url"));

try {
    Control[] connCtls = new Control[] {new FastBindConnectionControl()};
    ctx = new InitialLdapContext(env, connCtls);
    ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, "DOMAIN\\" + username);
    ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
    ctx.reconnect(connCtls);
    /* TODO: Only return true if user is in group "ABC" */
    return true; //User authenticated
} catch (Exception e) {
    return false; //User could NOT be authenticated
} finally {
    ...
}

更新:请见下文解决方法。

11个回答

22

我们使用下面的类来解决这个问题。只需调用authenticate方法:

import java.text.MessageFormat;
import java.util.*;    
import javax.naming.*;    
import org.apache.log4j.Level;

public class LdapGroupAuthenticator {
    public static final String DISTINGUISHED_NAME = "distinguishedName";
    public static final String CN = "cn";
    public static final String MEMBER = "member";
    public static final String MEMBER_OF = "memberOf";
    public static final String SEARCH_BY_SAM_ACCOUNT_NAME = "(SAMAccountName={0})";
    public static final String SEARCH_GROUP_BY_GROUP_CN = "(&(objectCategory=group)(cn={0}))";

    /*
     * Prepares and returns CN that can be used for AD query
     * e.g. Converts "CN=**Dev - Test Group" to "**Dev - Test Group"
     * Converts CN=**Dev - Test Group,OU=Distribution Lists,DC=DOMAIN,DC=com to "**Dev - Test Group"
     */
    public static String getCN(String cnName) {
        if (cnName != null && cnName.toUpperCase().startsWith("CN=")) {
            cnName = cnName.substring(3);
        }
        int position = cnName.indexOf(',');
        if (position == -1) {
            return cnName;
        } else {
            return cnName.substring(0, position);
        }
    }
    public static boolean isSame(String target, String candidate) {
        if (target != null && target.equalsIgnoreCase(candidate)) {
            return true;
        }
        return false;
    }

    public static boolean authenticate(String domain, String username, String password) {
        Hashtable env = new Hashtable();
        env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.PROVIDER_URL, "ldap://1.2.3.4:389");
        env.put(Context.SECURITY_AUTHENTICATION, "simple");
        env.put(Context.SECURITY_PRINCIPAL, domain + "\\" + username);
        env.put(Context.SECURITY_CREDENTIALS, password);
        DirContext ctx = null;
        String defaultSearchBase = "DC=DOMAIN,DC=com";
        String groupDistinguishedName = "DN=CN=DLS-APP-MyAdmin-C,OU=DLS File Permissions,DC=DOMAIN,DC=com";

        try {
            ctx = new InitialDirContext(env);

            // userName is SAMAccountName
            SearchResult sr = executeSearchSingleResult(ctx, SearchControls.SUBTREE_SCOPE, defaultSearchBase,
                    MessageFormat.format( SEARCH_BY_SAM_ACCOUNT_NAME, new Object[] {username}),
                    new String[] {DISTINGUISHED_NAME, CN, MEMBER_OF}
                    );

            String groupCN = getCN(groupDistinguishedName);
            HashMap processedUserGroups = new HashMap();
            HashMap unProcessedUserGroups = new HashMap();

            // Look for and process memberOf
            Attribute memberOf = sr.getAttributes().get(MEMBER_OF);
            if (memberOf != null) {
                for ( Enumeration e1 = memberOf.getAll() ; e1.hasMoreElements() ; ) {
                    String unprocessedGroupDN = e1.nextElement().toString();
                    String unprocessedGroupCN = getCN(unprocessedGroupDN);
                    // Quick check for direct membership
                    if (isSame (groupCN, unprocessedGroupCN) && isSame (groupDistinguishedName, unprocessedGroupDN)) {
                        Log.info(username + " is authorized.");
                        return true;
                    } else {
                        unProcessedUserGroups.put(unprocessedGroupDN, unprocessedGroupCN);
                    }
                }
                if (userMemberOf(ctx, defaultSearchBase, processedUserGroups, unProcessedUserGroups, groupCN, groupDistinguishedName)) {
                    Log.info(username + " is authorized.");
                    return true;
                }
            }

            Log.info(username + " is NOT authorized.");
            return false;
        } catch (AuthenticationException e) {
            Log.info(username + " is NOT authenticated");
            return false;
        } catch (NamingException e) {
            throw new SystemException(e);
        } finally {
            if (ctx != null) {
                try {
                    ctx.close();
                } catch (NamingException e) {
                    throw new SystemException(e);
                }
            }
        }
    }

    public static boolean userMemberOf(DirContext ctx, String searchBase, HashMap processedUserGroups, HashMap unProcessedUserGroups, String groupCN, String groupDistinguishedName) throws NamingException {
        HashMap newUnProcessedGroups = new HashMap();
        for (Iterator entry = unProcessedUserGroups.keySet().iterator(); entry.hasNext();) {
            String  unprocessedGroupDistinguishedName = (String) entry.next();
            String unprocessedGroupCN = (String)unProcessedUserGroups.get(unprocessedGroupDistinguishedName);
            if ( processedUserGroups.get(unprocessedGroupDistinguishedName) != null) {
                Log.info("Found  : " + unprocessedGroupDistinguishedName +" in processedGroups. skipping further processing of it..." );
                // We already traversed this.
                continue;
            }
            if (isSame (groupCN, unprocessedGroupCN) && isSame (groupDistinguishedName, unprocessedGroupDistinguishedName)) {
                Log.info("Found Match DistinguishedName : " + unprocessedGroupDistinguishedName +", CN : " + unprocessedGroupCN );
                return true;
            }
        }

        for (Iterator entry = unProcessedUserGroups.keySet().iterator(); entry.hasNext();) {
            String  unprocessedGroupDistinguishedName = (String) entry.next();
            String unprocessedGroupCN = (String)unProcessedUserGroups.get(unprocessedGroupDistinguishedName);

            processedUserGroups.put(unprocessedGroupDistinguishedName, unprocessedGroupCN);

            // Fetch Groups in unprocessedGroupCN and put them in newUnProcessedGroups
            NamingEnumeration ns = executeSearch(ctx, SearchControls.SUBTREE_SCOPE, searchBase,
                    MessageFormat.format( SEARCH_GROUP_BY_GROUP_CN, new Object[] {unprocessedGroupCN}),
                    new String[] {CN, DISTINGUISHED_NAME, MEMBER_OF});

            // Loop through the search results
            while (ns.hasMoreElements()) {
                SearchResult sr = (SearchResult) ns.next();

                // Make sure we're looking at correct distinguishedName, because we're querying by CN
                String userDistinguishedName = sr.getAttributes().get(DISTINGUISHED_NAME).get().toString();
                if (!isSame(unprocessedGroupDistinguishedName, userDistinguishedName)) {
                    Log.info("Processing CN : " + unprocessedGroupCN + ", DN : " + unprocessedGroupDistinguishedName +", Got DN : " + userDistinguishedName +", Ignoring...");
                    continue;
                }

                Log.info("Processing for memberOf CN : " + unprocessedGroupCN + ", DN : " + unprocessedGroupDistinguishedName);
                // Look for and process memberOf
                Attribute memberOf = sr.getAttributes().get(MEMBER_OF);
                if (memberOf != null) {
                    for ( Enumeration e1 = memberOf.getAll() ; e1.hasMoreElements() ; ) {
                        String unprocessedChildGroupDN = e1.nextElement().toString();
                        String unprocessedChildGroupCN = getCN(unprocessedChildGroupDN);
                        Log.info("Adding to List of un-processed groups : " + unprocessedChildGroupDN +", CN : " + unprocessedChildGroupCN);
                        newUnProcessedGroups.put(unprocessedChildGroupDN, unprocessedChildGroupCN);
                    }
                }
            }
        }
        if (newUnProcessedGroups.size() == 0) {
            Log.info("newUnProcessedGroups.size() is 0. returning false...");
            return false;
        }

        //  process unProcessedUserGroups
        return userMemberOf(ctx, searchBase, processedUserGroups, newUnProcessedGroups, groupCN, groupDistinguishedName);
    }

    private static NamingEnumeration executeSearch(DirContext ctx, int searchScope,  String searchBase, String searchFilter, String[] attributes) throws NamingException {
        // Create the search controls
        SearchControls searchCtls = new SearchControls();

        // Specify the attributes to return
        if (attributes != null) {
            searchCtls.setReturningAttributes(attributes);
        }

        // Specify the search scope
        searchCtls.setSearchScope(searchScope);

        // Search for objects using the filter
        NamingEnumeration result = ctx.search(searchBase, searchFilter,searchCtls);
        return result;
    }

    private static SearchResult executeSearchSingleResult(DirContext ctx, int searchScope,  String searchBase, String searchFilter, String[] attributes) throws NamingException {
        NamingEnumeration result = executeSearch(ctx, searchScope,  searchBase, searchFilter, attributes);

        SearchResult sr = null;
        // Loop through the search results
        while (result.hasMoreElements()) {
            sr = (SearchResult) result.next();
            break;
        }
        return sr;
    }
}

3
请注意,这并不完整,因为在Active Directory中,组可以嵌套,因此您将无法仅凭成为另一个组成员的身份获得组成员的用户。正确的方法是获取用户的tokenGroups属性,其中包含以SID列表形式表示的组,但这是一个需要解码的二进制属性。请参见:http://blogs.msdn.com/alextch/archive/2007/06/18/sample-java-application-that-retrieves-group-membership-of-an-active-directory-user-account.aspx - Dean Povey

18

上述代码片段都对我没用。在花了一整天的时间在谷歌和tomcat源代码上之后,下面的代码成功地找到了用户组。

import java.util.Hashtable;

import javax.naming.CompositeName;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.NameParser;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;

public class MemberOfTest{
    private static final String contextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
    private static final String connectionURL = "ldap://HOST:PORT";
    private static final String connectionName = "CN=Query,CN=Users,DC=XXX,DC=XX";
    private static final String connectionPassword = "XXX";

    // Optioanl
    private static final String authentication = null;
    private static final String protocol = null;

    private static String username = "XXXX";

    private static final String MEMBER_OF = "memberOf";
    private static final String[] attrIdsToSearch = new String[] { MEMBER_OF };
    public static final String SEARCH_BY_SAM_ACCOUNT_NAME = "(sAMAccountName=%s)";
    public static final String SEARCH_GROUP_BY_GROUP_CN = "(&(objectCategory=group)(cn={0}))";
    private static String userBase = "DC=XXX,DC=XXX";

    public static void main(String[] args) throws NamingException {
        Hashtable<String, String> env = new Hashtable<String, String>();

        // Configure our directory context environment.

        env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactory);
        env.put(Context.PROVIDER_URL, connectionURL);
        env.put(Context.SECURITY_PRINCIPAL, connectionName);
        env.put(Context.SECURITY_CREDENTIALS, connectionPassword);
        if (authentication != null)
            env.put(Context.SECURITY_AUTHENTICATION, authentication);
        if (protocol != null)
            env.put(Context.SECURITY_PROTOCOL, protocol);

        InitialDirContext   context = new InitialDirContext(env);
        String              filter  = String.format(SEARCH_BY_SAM_ACCOUNT_NAME, username);
        SearchControls      constraints = new SearchControls();
        constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
        constraints.setReturningAttributes(attrIdsToSearch);
        NamingEnumeration results = context.search(userBase, filter,constraints);
        // Fail if no entries found
        if (results == null || !results.hasMore()) {
            System.out.println("No result found");
            return;
        }

        // Get result for the first entry found
        SearchResult result = (SearchResult) results.next();

        // Get the entry's distinguished name
        NameParser parser = context.getNameParser("");
        Name contextName = parser.parse(context.getNameInNamespace());
        Name baseName = parser.parse(userBase);

        Name entryName = parser.parse(new CompositeName(result.getName())
                .get(0));

        // Get the entry's attributes
        Attributes attrs = result.getAttributes();
        Attribute attr = attrs.get(attrIdsToSearch[0]);

        NamingEnumeration e = attr.getAll();
        System.out.println("Member of");
        while (e.hasMore()) {
            String value = (String) e.next();
            System.out.println(value);
        }
    }
}

1
Marcus的解决方案包含未知的类/库(在导入它们之前)和其他无关的代码,而这个解决方案简单且运行良好。谢谢! - elady

6
最简单的方法是使用“lookup”:(打开Ldap上下文:请参见上面的示例)
 /**
  * Tests if an Active Directory user exists in an Active Directory group. 
  * @param ctx LDAP Context.
  * @param dnADGroup distinguishedName of group.
  * @param dnADUser distinguishedName of user.
  * @return True if user is member of group.
  */


public static boolean isMemberOfADGroup(LdapContext ctx, String dnADGroup, String dnADUser) {
  try {
   DirContext lookedContext = (DirContext) (ctx.lookup(dnADGroup));
   Attribute attrs = lookedContext.getAttributes("").get("member");
   for (int i = 0; i < attrs.size(); i++) {
    String foundMember = (String) attrs.get(i);
    if(foundMember.equals(dnADUser)) {
     return true;
    }
   }
  } catch (NamingException ex) {
   String msg = "There has been an error trying to determin a group membership for AD user with distinguishedName: "+dnADUser;
   System.out.println(msg);
   ex.printStackTrace();
  }
  return false;
 }

注意:如果条目数量>1500,则此操作无法执行。 - PCB

3

LDAP查找用户是否为组成员的方法不正确,特别是针对已登录用户。对于实际登录的用户,组列表因用户登录的计算机而异。该列表需要包括来自域信任、嵌套组和本地组的组。

如果您想在Java中查找当前已登录用户或使用用户名和密码登录的用户的组成员资格,请尝试Waffle

IWindowsAuthProvider prov = new WindowsAuthProviderImpl();
IWindowsIdentity identity = prov.logonUser("username", "password"); 
System.out.println("User identity: " + identity.getFqn()); 
for(IWindowsAccount group : identity.getGroups()) { 
    System.out.println(" " + group.getFqn() + " (" + group.getSidString() + ")"); 
} 

这对我来说看起来很有前途。但是我在这里发现了一个问题。我创建了一个名为“Test”的本地组。在此组下,我手动添加了当前用户。当我运行上面的代码时,它没有显示出“Test”组。任何指针都会对我有所帮助。 - Santosh Panda
请确保它是一个安全组,https://technet.microsoft.com/zh-cn/library/cc781446(v=ws.10).aspx。 - dB.

3

继Sundaramurthi的回答之后,甚至可以更加简单地完成,您不需要查询所有用户组:

(&(objectClass=user)(sAMAccountName=XXXX)(memberOf=CN=YYY,OU=_Common-Access,OU=Groups,OU=_CORP,DC=XXX,DC=XX))

其中 XXXX - 用户名     XXX.XX - 域名     YYY - 组名

这允许您仅获取答案,即用户是否在组内。

所以只需执行:

String userBase = "DC=XXX,DC=XX";
String CHECK_IF_USER_IN_GROUP = "(&(objectClass=user)(sAMAccountName=%s)(memberOf=CN=%s,OU=...,OU=...,OU=...,%s))";

String queryFilter = String.format(CHECK_IF_USER_IN_GROUP, user, group, userBase);

SearchControls constraints = new SearchControls();
constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);

NamingEnumeration results = context.search(userBase, queryFilter, constraints);

if (results == null) {
    throw new Exception("No answer from LDAP");
}

if (!results.hasMore()) {
    System.out.println("No result found");
    // user is not in the group
}
// user is in the group

2

我不确定具体的Java API是什么,但通用的方法是在查询/绑定中添加一个组检查。


1

我无法提供使用Java命名LDAP的工作代码。 我使用了Spring LDAP,做法如下: 获取用户对象,在用户名上执行搜索,类似于sAMAccountName=USERNAME

在获取对象后,检索属性memberOf -> 这将是一个列表,并在Java中检查特定的列表。

这是我能想到的唯一方法。


1

在建立连接后,如果您想查询用户是否属于特定组,可以使用Active Directory中的member或memberOf属性进行查询。

使用memberOf属性: 使用的过滤器:(&(Group Member Attribute=Group DN)(objectClass=Group Object class)) 示例:(&(memberOf=CN=group,ou=qa_ou,dc=ppma,dc=org)(objectClass=group))

使用member属性: 使用的过滤器:(&(Group Member Attribute=User DN)(objectClass=Group Object class)) 示例:(&(member=CN=user,ou=qa_ou,dc=ppma,dc=org)(objectClass=group))


0
我发现这个很有用: 检索Active Directory的组成员资格 而且我有这段可用的代码:
import java.util.Hashtable;
import javax.naming.CompositeName;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.NameParser;
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.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;

public class TestAD1 {

    private static String userBase = "DC=SomeName,DC=SomeName,DC=SomeName,DC=SomeName,DC=COM,DC=US";

    public static void main(String[] args) {
        TestAD1 tad = new TestAD1();
        try {
            // Create a LDAP Context
            Hashtable env = new Hashtable();
            env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
            env.put(Context.SECURITY_AUTHENTICATION, "simple");
            env.put(Context.SECURITY_PRINCIPAL, "ldap.serviceaccount@domain.com");
            env.put(Context.SECURITY_CREDENTIALS, "drowssap");
            env.put(Context.PROVIDER_URL, "ldap://fully.qualified.server.name:389");
            LdapContext ctx = new InitialLdapContext(env, null);
            InitialDirContext inidircontext = new InitialDirContext(env);
            DirContext dirctx = new InitialLdapContext(env, null);
            System.out.println("Connection Successful.");

            // Print all attributes of the name in namespace
            SearchControls sctls = new SearchControls();
            String retatts[] = {"sn", "mail", "displayName", "sAMAccountName"};
            sctls.setReturningAttributes(retatts);
            sctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
            String srchfilter = "(&(objectClass=user)(mail=*))";
            String srchbase = userBase;
            int totalresults = 0;

            NamingEnumeration answer = dirctx.search(srchbase, srchfilter, sctls);
            while (answer.hasMoreElements()) {
                SearchResult sr = (SearchResult) answer.next();
                totalresults++;
                System.out.println(">>>  " + sr.getName());

                Attributes attrs = sr.getAttributes();
                if (answer == null || !answer.hasMore()) {
                    System.out.println("No result found");
                    return;
                }

                if (attrs != null) {
                    try {
                        System.out.println("    surname: " + attrs.get("sn").get());
                        System.out.println("    Email - ID: " + attrs.get("mail").get());
                        System.out.println("    User - ID: " + attrs.get("displayName").get());
                        System.out.println("    Account Name: " + attrs.get("sAMAccountName").get());
                        tad.GetGroups(inidircontext, attrs.get("sAMAccountName").get().toString());
                    } catch (NullPointerException e) {
                        System.out.println("Error listing attributes..." + e);

                    }
                }
                System.out.println("Total Results : " + totalresults);
                // close dir context
                dirctx.close();
            }

            ctx.close();
        } catch (NamingException e) {
            System.out.println("Problem Search Active Directory..." + e);
            //e.printStackTrace();
        }

    }

    // Get all the groups.

    public void GetGroups(InitialDirContext context, String username) throws NamingException {
        String[] attrIdsToSearch = new String[]{"memberOf"};
        String SEARCH_BY_SAM_ACCOUNT_NAME = "(sAMAccountName=%s)";
        String filter = String.format(SEARCH_BY_SAM_ACCOUNT_NAME, username);
        SearchControls constraints = new SearchControls();
        constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
        constraints.setReturningAttributes(attrIdsToSearch);
        NamingEnumeration results = context.search(userBase, filter, constraints);
        // Fail if no entries found
        if (results == null || !results.hasMore()) {
            System.out.println("No result found");
            return;
        }
        SearchResult result = (SearchResult) results.next();
        Attributes attrs = result.getAttributes();
        Attribute attr = attrs.get(attrIdsToSearch[0]);

        NamingEnumeration e = attr.getAll();
        System.out.println(username + " is Member of the following groups  : \n");
        while (e.hasMore()) {
            String value = (String) e.next();
            System.out.println(value);
        }
    }

}

0

不幸的是,答案因AD的安装以及其他类型的LDAP服务器而异。

我们不得不解决同样的问题。最终,我们允许系统管理员向我们提供LDAP查询模式,在模式中替换用户名(如果需要变量,则还包括组名)。


谢谢。为什么答案会有所不同?这似乎是一个非常标准的查询。你是否有使用LDAP查询的代码示例? - Marcus Leon
我怀疑这取决于AD的版本,因为AD如何处理memberOf。它将组成员关系存储在组上而不是用户对象上。用户的memberOf属性实际上并不存在,而是动态计算的。更好的LDAP服务器会在两个位置都存储它,正如你所期望的那样。 - geoffc

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