LDAP 디렉터리 서버는 읽기에 최적화된 계층적 데이터 저장소입니다. 일반적으로 계정 인증 및 권한 부여에 필요한 계정 관련 정보를 저장하는 데 사용됩니다.
이전 게시물에서는 AD와 LDAP을 비교하면서 LDAP 구조에 대해 알아봤는데, 이번에는 스프링에서 계정 인증 및 검색하고 디렉터리 서버에서 계정 생성 및 수정하기 위한 Spring LDAP API 사용법을 알아보겠습니다.
Maven Dependency
<dependency>
<groupId>org.springframework.ldap</groupId>
<artifactId>spring-ldap-core</artifactId>
<version>...</version>
</dependency>
해당 dependency의 버전은 https://search.maven.org/search?q=a:spring-ldap-core 에서 확인 가능합니다.
LDAP 데이터
계정 인증, 검색, 생성, 수정을 위한 구조는 이전 게시물에서 언급했던 dn 정보 "cn=Tom, ou=people, o=zoho, c=india"를 사용하겠습니다.
Attribute | Value |
objectClass | top |
objectClass | organizationalPerson |
objectClass | person |
cn | Tom |
distinguishedName | CN=Tom,OU=people,O=zoho,C=india |
displayName | 톰 |
userPrincipalName | tom@moonsiri.tistory.com |
userAccountControl | 512 |
sAMAccountName | Tom |
badPwdCount | 0 |
lockoutTime | 0 |
memberOf | ... |
... | ... |
Spring LDAP API
스프링에서 ldap api를 사용할 수 있는 방법은 두 가지가 있습니다.
1안) ContextSource 및 LdapTemplete Bean 정의
ContextSource는 LdapTemplate을 만드는 데 사용됩니다.
@Bean
public LdapContextSource contextSource() {
LdapContextSource contextSource = new LdapContextSource();
contextSource.setUrl("ldap://localhost:636");
contextSource.setBase("ou=people,o=zoho,c=india");
contextSource.setUserDn("cn=admin,ou=admin,o=zoho,c=india");
contextSource.setPassword("password");
return contextSource;
}
LdapTemplate은 LDAP 항목의 생성 및 수정에 사용됩니다.
@Bean
public LdapTemplate ldapTemplate() {
return new LdapTemplate(contextSource());
}
추가로 Ldap Pool을 설정할 수 있는데 본문에서는 제외하겠습니다.
2안) Spring boot starter 사용
스프링 부트 환경에서 Spring Boot Starter Data Ldap 종속성을 사용하면 LdapContextSource 및 LdapTemplate을 자동으로 주입할 수 있습니다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-ldap</artifactId>
</dependency>
위 dependency를 추가한 뒤 application.yml에 ldap 정보를 구성하면 됩니다.
spring:
ldap:
url: ldap://localhost:636
base: ou=people,o=zoho,c=india
username: cn=admin,ou=admin,o=zoho,c=india
password: password
Spring framework에 LDAP 설정이 완료되었으니 이번엔 LdapTemplate을 사용하여 LDAP 데이터 CRUD 기능을 확인해보겠습니다. (MS AD 기준으로 작성되었으며, OpenLDAP을 사용할 경우 다를 수 있습니다.)
@Resource
private LdapTemplate ldapTemplate;
계정 생성
public void createLdapUser(String username) {
Object createUser = ADUserSchemaVO.builder() // ADUserSchemaVO는 편의를 위해 구현한 객체
.cn("Tom")
.displayName("톰")
.userPrincipalName("Tom@moonsiri.tistory.com")
.userAccountControl("544") // 비밀번호가 셋팅되기 전 활성화 상태
.sAMAccountName("Tom")
.build();
ldapTemplate.create(createUser);
}
계정 검색
public ADUserSchemaVO findLdapUser(String username) {
LdapQuery qry = LdapQueryBuilder.query().where("cn").is(username);
return ldapTemplate.findOne(qry, ADUserSchemaVO.class);
}
ldapTemplate의 base가 Tom의 dn suffix로 설정되어있기 때문에 cn으로만 Tom을 찾을 수 있습니다.
계정 수정
계정 비밀번호 설정
ldap 수정 기능을 사용하여 계정 비밀번호 설정 방법입니다.
public void setLdapPassword(String username, String password) {
String userCn = "CN=" + username;
String newQuotedPassword = "\"" + password + "\"";
byte[] newUnicodePassword = newQuotedPassword.getBytes(StandardCharsets.UTF_16LE.name());
ModificationItem[] mods = new ModificationItem[1];
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute("unicodePwd", newUnicodePassword));
ldapTemplate.modifyAttributes(userCn, mods);
}
계정 잠금 해제
이번엔 비밀번호 오류 횟수 초과 정책에 의해 계정이 잠겼을 경우 잠금을 풀기 위해 계정 정보를 수정해보겠습니다.
public void clearBadPasswordBlock(String username) {
ADUserSchemaVO adUserVO = findLdapUser(username);
DirContextOperations context = ldapTemplate.lookupContext(adUserVO.getDn());
// badPwdCount 를 0으로 초기화 -> lockoutTime을 0으로 초기화(순서 중요)
// context.setAttributeValue("badPwdCount", "0");
context.setAttributeValue("lockoutTime", "0");
ldapTemplate.modifyAttributes(context);
}
계정 삭제
public void deleteLdapUser(String username) {
ADUserSchemaVO object = ADUserSchemaVO.builder().cn(username).build();
ldapTemplate.delete(object);
}
계정 인증
public void ldapAuthenticate(String username, String password) {
// AD의 경우 유저 엔티티 ObjectClass에 person이 생성됨
LdapQuery query = query().where("cn").is(username).and("objectClass").is("person");
try {
ldapTemplate.authenticate(query, password);
} catch (Exception e) {
int errorCd = getLdapDecErrorCd(e);
System.out.println(errorCd);
}
}
만약 인증 중에 오류가 발생하면 오류 메시지에 오류코드를 HEX값으로 리턴하는데, 해당 오류코드는 아래 URL표의 hex값과 비교하면 됩니다.
The exception is [LDAP: error code 49 - 80090308: LdapErr: DSID-0Cxxxxxx, comment: AcceptSecurityContext error, data <HEX>, vece ].
https://ldapwiki.com/wiki/Common%20Active%20Directory%20Bind%20Errors
public int getLdapDecErrorCd(Exception exception) {
// Exception에서 message 조회
String message = exception.getMessage();
// LDAP 에러 메세지에서 코드를 파싱하는 정규식으로 hex 코드 조회
Matcher m = Pattern.compile(".*data\\s([0-9a-f]{3,4}).*").matcher(message);
if (m.matches()) {
return Integer.parseInt(m.group(1), 16); // 에러코드를 dec로 파싱
} else {
return -1;
}
}
[Reference]
https://www.baeldung.com/spring-ldap
댓글