자바 애플리케이션의 LDAP 설정 파일에 INITIAL_CONTEXT_FACTORY · PROVIDER_URL 같은 항목이 있는데 무엇을 넣어야 하는지 모호하다. 또 사용자 수가 많은 Active Directory 에서 검색하면 1000 건에서 잘려 나온다.
INITIAL_CONTEXT_FACTORY 는 JDK 안의 자바 클래스 이름이다. LDAP 서버에서 찾아올 값이 아니며, 서버가 OpenLDAP 이든 Active Directory 이든 값은 같다.
com.sun.jndi.ldap.LdapCtxFactory
서버 주소가 들어가는 항목은 PROVIDER_URL 이다.
ldap://dc1.example.local:389
ldaps://dc1.example.local:636
Active Directory 에서 포리스트 전체를 한 번에 훑어야 하면 글로벌 카탈로그 포트를 쓴다. 다만 글로벌 카탈로그에는 일부 속성만 복제되므로, 필요한 속성이 빠져 있지 않은지 확인해야 한다.
ldap://dc1.example.local:3268
ldaps://dc1.example.local:3269
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import java.util.Hashtable;
Hashtable<String, Object> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://dc1.example.local:389");
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, "CN=svc-app,OU=Service,DC=example,DC=local");
env.put(Context.SECURITY_CREDENTIALS, System.getenv("LDAP_BIND_PASSWORD"));
env.put("com.sun.jndi.ldap.connect.timeout", "5000");
env.put("com.sun.jndi.ldap.read.timeout", "30000");
LdapContext ctx = new InitialLdapContext(env, null);
SECURITY_PRINCIPAL 은 AD 라면 전체 DN 대신 user@example.local 형태의 UPN 이나 EXAMPLE\user 형태도 받는다. OpenLDAP 은 DN 만 받는다.
바인드 비밀번호는 설정 파일이나 소스에 적지 않고 환경변수 · 자격증명 저장소에서 읽는다.
Active Directory 는 MaxPageSize 정책 때문에 한 번의 검색 응답을 기본 1000 건으로 끊는다. 클라이언트가 페이지 제어를 쓰지 않으면 그 1000 건만 받고 끝난다. 잘렸다는 예외도 나지 않아서 "사용자가 1000 명뿐"이라고 잘못 읽기 쉽다.
RFC 2696 의 PagedResultsControl 을 걸고, 응답으로 돌아온 쿠키를 다음 요청에 다시 실어 보내는 반복문을 돌려야 한다.
import javax.naming.ldap.Control;
import javax.naming.ldap.PagedResultsControl;
import javax.naming.ldap.PagedResultsResponseControl;
int pageSize = 500;
byte[] cookie = null;
SearchControls sc = new SearchControls();
sc.setSearchScope(SearchControls.SUBTREE_SCOPE);
sc.setReturningAttributes(new String[]{"sAMAccountName", "mail", "memberOf"});
do {
ctx.setRequestControls(new Control[]{
new PagedResultsControl(pageSize, cookie, Control.NONCRITICAL)
});
NamingEnumeration<SearchResult> results =
ctx.search("OU=Users,DC=example,DC=local", "(objectClass=user)", sc);
while (results.hasMore()) {
SearchResult r = results.next();
// ... 처리
}
cookie = null;
Control[] responses = ctx.getResponseControls();
if (responses != null) {
for (Control c : responses) {
if (c instanceof PagedResultsResponseControl) {
cookie = ((PagedResultsResponseControl) c).getCookie();
}
}
}
} while (cookie != null && cookie.length != 0);
ctx.close();
첫 요청에서는 쿠키가 null 이고, 마지막 페이지의 응답 쿠키는 길이 0 인 배열로 온다. 이 두 경우를 구분하지 않으면 무한 반복이 된다.
Control.NONCRITICAL 은 서버가 이 제어를 몰라도 검색을 그냥 진행하라는 뜻이다. CRITICAL 로 두면 제어를 지원하지 않는 서버에서 검색 자체가 실패한다.
베이스 DN 과 속성 이름은 서버에서 직접 확인하는 것이 빠르다.
ldapsearch -x -H ldap://dc1.example.local -D 'svc-app@example.local' -W \
-b '' -s base namingContexts
ldapsearch -x -H ldap://dc1.example.local -D 'svc-app@example.local' -W \
-b 'DC=example,DC=local' '(sAMAccountName=someuser)'
윈도우 쪽에서는 dsa.msc 의 속성 편집기, Get-ADUser -Properties *, Apache Directory Studio 로 같은 값을 볼 수 있다.
AD 의 현재 페이지 크기 정책은 다음으로 본다.
ntdsutil "LDAP policies" connections "connect to server dc1" quit "show values" quit quit
ldaps:// 를 쓰면 서버 인증서를 자바 트러스트스토어에 넣어야 한다. 넣지 않으면 PKIX path building failed 가 난다.
간이 바인드(simple bind)는 비밀번호를 평문으로 보낸다. 평문 389 포트에서 simple bind 를 쓰는 구성은 AD 의 LDAP 서명 강제 정책이 켜지는 순간 통째로 끊긴다. StartTLS 나 LDAPS 로 옮기는 것을 전제로 설계한다.
memberOf 는 중첩 그룹을 펼쳐 주지 않는다. 중첩까지 필요하면 AD 전용 매칭 규칙 1.2.840.113556.1.4.1941 을 쓴다.