【Java】ActiveDirectoryの参照/更新


はじめに

ADのユーザーをメンテナンスする機能を提供するため、ユーザー一覧を取得したり、更新できるようにしたい。
JavaではJNDIを使ってADへアクセスできる。

ADの参照

単一ユーザーの取得

AD認証などで、ユーザーIDを指定して検索する場合は、下記URL参照。

ユーザー一覧の取得

特定のOUに所属するユーザーの一覧など、結果が複数件あることが予想される検索の場合、LDAPのページサイズを意識する必要がある。
WindowsのActive Directoryの場合、おそらく1,000件が結果の最大サイズ。
それを超える件数を取得したい場合は、ページングをしながら検索を繰り返す。

ページングしながらユーザー一覧を取得するサンプル
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://server"); // ADサーバーのアドレス
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, "userid@domain"); // ADサーバーにログオンするためのユーザーID@ドメイン
env.put(Context.SECURITY_CREDENTIALS, "password"); // パスワード

final int PAGE_SIZE = 1000;

InitialLdapContext context = null;
try {
    context = new InitialLdapContext(env, null);

    // 検索条件の指定
    String name = "OU=aaa, DC=bbb, DC=ccc";
    String filter = "(objectCategory=user)";

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

    byte[] cookie = null;

    do {
        context.setRequestControls(new Control[] { new PagedResultsControl(PAGE_SIZE, cookie,
                Control.CRITICAL) });

        // 検索実行
        NamingEnumeration<SearchResult> results = context.search(name, filter, control);

        // 結果取得
        while (results.hasMore()) {
            SearchResult result = results.next();
            AdUserBean user = new AdUserBean(result);

            System.out.println(user);
        }

        // 次のページがあるかチェック
        cookie = null;

        if (context.getResponseControls() != null) {
            Optional<Control> found = Arrays.stream(context.getResponseControls())
                    .filter(c -> c instanceof PagedResultsResponseControl).findFirst();

            if (found.isPresent()) {
                PagedResultsResponseControl prrc = (PagedResultsResponseControl) found.get();
                cookie = prrc.getCookie();
            }
        }

    } while (cookie != null);

} finally {
    try {
        if (context != null) {
            context.close();
        }
    } catch (NamingException e) {
    }
}
AdUserBean
public class AdUserBean {

    private String cn = null;
    private String name = null;
    /** 名前 */
    private String displayName = null;
    /** ユーザーID+ドメイン */
    private String userPrincipalName = null;
    /** LDAP識別名 */
    private String distinguishedName = null;
    /** メールアドレス */
    private String mail = null;
    /** ポケットベル */
    private String pager = null;

    /**
     * コンストラクタ
     */
    public AdUserBean() {

    }


    /**
     * コンストラクタ
     *
     * Active Directory検索結果から内容を取得する
     *
     * @param result
     * @throws NamingException
     */
    public AdUserBean(SearchResult result) throws NamingException {
        NamingEnumeration<? extends Attribute> attributes = result.getAttributes().getAll();

        while (attributes.hasMoreElements()) {
            Attribute attribute = attributes.next();
            Object value = attribute.get();

            if ("name".equals(attribute.getID())) {
                if (value instanceof String) {
                    name = (String) value;
                }

            } else if ("displayName".equals(attribute.getID())) {
                if (value instanceof String) {
                    displayName = (String) value;
                }

            } else if ("cn".equals(attribute.getID())) {
                if (value instanceof String) {
                    cn = (String) value;
                }

            } else if ("userPrincipalName".equals(attribute.getID())) {
                if (value instanceof String) {
                    userPrincipalName = (String) value;
                }

            } else if ("pager".equals(attribute.getID())) {
                if (value instanceof String) {
                    pager = (String) value;
                }

            } else if ("mail".equals(attribute.getID())) {
                if (value instanceof String) {
                    mail = (String) value;
                }

            } else if ("distinguishedName".equals(attribute.getID())) {
                if (value instanceof String) {
                    distinguishedName = (String) value;
                }

            }
        }

    }

// 以下略
}

hasMorehasMoreElementsの違い

search()メソッドの戻り値であるNamingEnumerationクラスには、普通のEnumerationクラスにあるhasMoreElementsメソッドと、独自のhasMoreメソッドの2種類がある。

while (results.hasMore()) {
    SearchResult result = results.next();
    // 略
}

hasMoreメソッドを使うと、例えばページサイズを超えた件数を取得しようとしたときにNamingExceptionがスローされる。
https://docs.oracle.com/javase/jp/11/docs/api/java.naming/javax/naming/NamingEnumeration.html

ただし、先のサンプルのようにページングしながらADを検索する場合は、コード側で指定したページサイズ>サーバー側のページサイズ となっていてもエラーにならず、サーバー側のページサイズで検索結果が返される。

属性値の取り方

Active Directoryの属性には、単一の値を持つ項目と複数の値を持つ項目がある。
それぞれ取得方法が異なるので注意。

属性値の取得部分を抜粋
/** ポケットベル */
private String pager = null;
/** ポケットベル(その他) */
private List<String> otherPager = null;

public AdUserBean(SearchResult result) throws NamingException {
    NamingEnumeration<? extends Attribute> attributes = result.getAttributes().getAll();

    while (attributes.hasMoreElements()) {
        Attribute attribute = attributes.next();
        Object value = attribute.get();

        if ("pager".equals(attribute.getID())) {
            // ポケットベル(単一の項目)
            if (value instanceof String) {
                pager = (String) value;
            }

        } else if ("otherPager".equals(attribute.getID())) {
            // ポケットベル(その他) (複数の項目)
            NamingEnumeration<?> values = attribute.getAll();

            while (values.hasMore()) {
                if (value instanceof String) {
                    if (otherPager == null) {
                        otherPager = new ArrayList<String>();
                    }

                    value = values.next();

                    otherPager.add((String) value);
                }
            }

        }
    }
}

ユーザー属性の変更

ユーザー属性の変更
private static void update() throws NamingException {

    Hashtable<String, String> env = new Hashtable<String, String>();
    env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
    env.put(Context.PROVIDER_URL, "ldap://server"); // ADサーバーのアドレス
    env.put(Context.SECURITY_AUTHENTICATION, "simple");
    env.put(Context.SECURITY_PRINCIPAL, "user@domain"); // ADサーバーにログオンするためのユーザーID@ドメイン
    env.put(Context.SECURITY_CREDENTIALS, "password"); // パスワード

    DirContext context = null;
    try {
        context = new InitialDirContext(env);

        Attributes attrs = new BasicAttributes();
        attrs.put(new BasicAttribute("pager", "123456"));
        attrs.put(new BasicAttribute("telephoneNumber", "1234567"));

        String name = "CN=username, OU=xxx, DC=yyy, DC=zzz";

        context.modifyAttributes(name, DirContext.REPLACE_ATTRIBUTE, attrs);

    } finally {
        try {
            if (context != null) {
                context.close();
            }
        } catch (NamingException e) {
        }
    }
}

modifyAttributesの引数に指定するDNは、そのユーザー(オブジェクト)が属するOUなどを正確に指定しないといけない。OUの階層を省略したりすると、「オブジェクトが見つからない」とエラーになる。

属性や検索フィルターの参考

メールアドレスは単体項目だが、電話番号は複数指定可能な項目。

Active Directory のユーザーの属性についての説明(他にも属性の種類ごとに説明ページが分かれている)
https://docs.microsoft.com/en-us/windows/win32/ad/naming-properties

Active Directory の属性一覧
https://docs.microsoft.com/en-us/windows/win32/adschema/attributes-all

LDAP の検索フィルター構文
http://software.fujitsu.com/jp/manual/manualfiles/M050000/B1WN4911/01/idmgr07/idmgr447.htm
https://www.ibm.com/support/knowledgecenter/ja/SSYJ99_8.5.0/admin-system/rbug_ldapfltrxprns.html