LDAP
Lightweight Directory Access Protocol
디렉토리 서비스에 접근하기 위한 프로토콜로, 사용자 인증, 조직 구조 관리, 권한 부여 등 기업 환경의 중앙 집중식 사용자 관리에 널리 사용됩니다.
Lightweight Directory Access Protocol
디렉토리 서비스에 접근하기 위한 프로토콜로, 사용자 인증, 조직 구조 관리, 권한 부여 등 기업 환경의 중앙 집중식 사용자 관리에 널리 사용됩니다.
LDAP(Lightweight Directory Access Protocol)은 TCP/IP 기반의 디렉토리 서비스 접근 프로토콜입니다. X.500 표준을 간소화한 것으로, 읽기 연산에 최적화되어 있어 사용자 인증, 조직 정보 조회, 애플리케이션 설정 저장 등에 널리 사용됩니다. 기본 포트는 389(LDAP), 636(LDAPS-SSL/TLS)입니다.
LDAP 디렉토리는 트리 구조(DIT, Directory Information Tree)로 데이터를 저장합니다. 최상위부터 DC(Domain Component), OU(Organizational Unit), CN(Common Name) 등의 요소로 구성됩니다. 예를 들어 cn=홍길동,ou=engineering,dc=example,dc=com은 example.com 도메인의 engineering 부서에 속한 홍길동 사용자를 나타냅니다.
대표적인 LDAP 서버로는 Microsoft Active Directory(Windows 환경의 사실상 표준), OpenLDAP(오픈소스), 389 Directory Server(Red Hat), ApacheDS 등이 있습니다. 현대 환경에서는 AWS Directory Service, Azure AD 같은 클라우드 서비스도 LDAP 호환성을 제공합니다.
LDAP 인증의 일반적인 흐름은: 1) 사용자가 username/password 입력, 2) 애플리케이션이 LDAP 서버에 Bind 요청, 3) LDAP 서버가 인증 성공/실패 응답, 4) 필요 시 사용자 속성(그룹, 권한 등) 조회입니다. SSO(Single Sign-On) 환경에서는 Kerberos나 SAML과 함께 사용되기도 합니다.
from ldap3 import Server, Connection, ALL, SUBTREE
from ldap3.core.exceptions import LDAPBindError, LDAPException
from typing import Optional, List, Dict
# LDAP 서버 설정
LDAP_SERVER = "ldap://ldap.example.com:389"
LDAP_BASE_DN = "dc=example,dc=com"
LDAP_BIND_DN = "cn=admin,dc=example,dc=com"
LDAP_BIND_PASSWORD = "admin_password"
class LDAPClient:
"""LDAP 연동 클라이언트"""
def __init__(self, server_url: str, base_dn: str):
self.server = Server(server_url, get_info=ALL)
self.base_dn = base_dn
self.conn = None
def bind_admin(self, bind_dn: str, password: str) -> bool:
"""관리자 계정으로 바인딩"""
try:
self.conn = Connection(
self.server,
user=bind_dn,
password=password,
auto_bind=True
)
return True
except LDAPBindError:
return False
def authenticate_user(self, username: str, password: str) -> Optional[dict]:
"""사용자 인증 (Bind 방식)"""
# 1. 먼저 사용자 DN 검색
user_dn = self._find_user_dn(username)
if not user_dn:
return None
# 2. 사용자 계정으로 직접 바인딩 시도
try:
user_conn = Connection(
self.server,
user=user_dn,
password=password,
auto_bind=True
)
user_conn.unbind()
# 3. 인증 성공 시 사용자 정보 반환
return self.get_user_info(username)
except LDAPBindError:
return None # 인증 실패
def _find_user_dn(self, username: str) -> Optional[str]:
"""사용자 DN 검색"""
search_filter = f"(sAMAccountName={username})" # AD 환경
# OpenLDAP인 경우: f"(uid={username})"
self.conn.search(
search_base=self.base_dn,
search_filter=search_filter,
search_scope=SUBTREE,
attributes=['distinguishedName']
)
if self.conn.entries:
return str(self.conn.entries[0].distinguishedName)
return None
def get_user_info(self, username: str) -> Optional[dict]:
"""사용자 상세 정보 조회"""
search_filter = f"(sAMAccountName={username})"
self.conn.search(
search_base=self.base_dn,
search_filter=search_filter,
search_scope=SUBTREE,
attributes=[
'cn', 'mail', 'displayName', 'department',
'memberOf', 'telephoneNumber', 'title'
]
)
if not self.conn.entries:
return None
entry = self.conn.entries[0]
return {
'username': username,
'dn': str(entry.entry_dn),
'name': str(entry.cn) if entry.cn else None,
'email': str(entry.mail) if entry.mail else None,
'display_name': str(entry.displayName) if entry.displayName else None,
'department': str(entry.department) if entry.department else None,
'title': str(entry.title) if entry.title else None,
'groups': [str(g) for g in entry.memberOf] if entry.memberOf else []
}
def get_user_groups(self, username: str) -> List[str]:
"""사용자가 속한 그룹 목록 조회"""
user_info = self.get_user_info(username)
if not user_info:
return []
# memberOf에서 그룹 CN만 추출
groups = []
for group_dn in user_info.get('groups', []):
# "CN=Developers,OU=Groups,DC=example,DC=com" -> "Developers"
cn_part = group_dn.split(',')[0]
if cn_part.startswith('CN='):
groups.append(cn_part[3:])
return groups
def is_member_of(self, username: str, group_name: str) -> bool:
"""특정 그룹 멤버십 확인"""
groups = self.get_user_groups(username)
return group_name in groups
def search_users(self, search_term: str, limit: int = 100) -> List[dict]:
"""사용자 검색"""
search_filter = f"(&(objectClass=person)(|(cn=*{search_term}*)(mail=*{search_term}*)))"
self.conn.search(
search_base=self.base_dn,
search_filter=search_filter,
search_scope=SUBTREE,
attributes=['cn', 'mail', 'sAMAccountName'],
size_limit=limit
)
results = []
for entry in self.conn.entries:
results.append({
'username': str(entry.sAMAccountName) if entry.sAMAccountName else None,
'name': str(entry.cn) if entry.cn else None,
'email': str(entry.mail) if entry.mail else None,
})
return results
def close(self):
"""연결 종료"""
if self.conn:
self.conn.unbind()
# 사용 예시
if __name__ == "__main__":
# LDAP 클라이언트 초기화
ldap = LDAPClient(LDAP_SERVER, LDAP_BASE_DN)
# 관리자로 바인딩
if ldap.bind_admin(LDAP_BIND_DN, LDAP_BIND_PASSWORD):
print("관리자 바인딩 성공")
# 사용자 인증 테스트
user = ldap.authenticate_user("hong.gildong", "user_password")
if user:
print(f"인증 성공: {user['display_name']} ({user['email']})")
print(f"소속 그룹: {ldap.get_user_groups('hong.gildong')}")
# 권한 확인
if ldap.is_member_of("hong.gildong", "Administrators"):
print("관리자 권한 있음")
else:
print("인증 실패")
# 사용자 검색
users = ldap.search_users("개발")
for u in users:
print(f"검색 결과: {u['name']} - {u['email']}")
ldap.close()