はじめに
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) {
}
}
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;
}
}
}
}
// 以下略
}
hasMore
とhasMoreElements
の違い
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