코알못

내 웹페이지에 LDAP을 연동 해보자! 본문

JAVA

내 웹페이지에 LDAP을 연동 해보자!

코린이s 2022. 5. 1. 12:03
728x90



내 웹페이지의 로그인 할 때 ldap 을 연동하여 로그인 기능을 구현할 것이며

ldap 에 정의된 회원만 내 웹페이지 기능을 이용할 수 있도록 한다.

우선 웹페이지와 ldap 연동을 어떻게 하는지 보기 위해 아래 저자가 만든 git 소스를 통해 파악한다.

스타터 코드라 기본적으로 있어야 하는 코드만 넣어 심플하다.

- https://github.com/works-code/embedded-ldap-login

 

GitHub - works-code/embedded-ldap-login: 임베디드 ldap을 이용한 로그인 구현을 위한 스타터 코드

임베디드 ldap을 이용한 로그인 구현을 위한 스타터 코드. Contribute to works-code/embedded-ldap-login development by creating an account on GitHub.

github.com

파악을 완료 했다면 위에 코드를 실행 시켜 어플리케이션을 띄워 ldap 서버를 활성화 시킨다.

ldap 관리를 쉽게 하기 위한 툴인 apacheDirectoryStudio를 아래와 같이 설치한뒤 실행시켜 본다.

$ brew install apache-directory-studio
############ 100.0%
==> Installing Cask apache-directory-studio
==> Moving App 'ApacheDirectoryStudio.app' to '/Applications
🍺  apache-directory-studio was successfully installed!

실행한 화면은 아래와 같다.

noAuth 를 눌러 인증 하지 않도록 하며(스타터 코드에 있는대로 하면 인증이 따로 없다)

fetch Base DNs 를 눌러 정상적으로 base dn 을 가져오는지 확인 한뒤 finish 를 눌러 완료한다.

정상적으로 가져온것을 볼 수 있다.

이제 ldap 스타터 코드를 실행시켜 첫 화면 (http://localhost:8080) 을 접속하면 로그인 페이지가 나오며

readme 에 나온 방법대로 corin/admin@1234 입력하여 로그인 하면 완료 문구가 노출 된다.

이제 임베디드 ldap 이 아닌 저번 시간에 직접 구성한 ldap 을 연동 하기로 한다.

만약 구축해둔 LDAP 이 없다면 아래 저자글 참고하여 만든 뒤 실습 진행하면 된다.

https://co-de.tistory.com/114

 

5분 안에 구축하는 LDAP

Microsoft 에서 개발한 AD(Active Directory)는 디렉토리 서비스 공급자이며 윈도우OS 에서 디렉터리 안에 내용을 쉽게 검색 할 수 있도록 한다. 이런 공급자들이 쉽게 통신을 하기 위한 프로토콜이 LDAP(Li

co-de.tistory.com

이제 apacheDirectoryStudio에서 구축한 ldap 서버에 접속한뒤 스타터 코드에서 만들어둔 AD 대로 다시 만들어본다.

유저 체크 할시 uid 속성을 통해 id를 체크하나 uid 속성을 포함하는 'inetOrgPerson' 클래스가 없기에 추가 해줘야한다. 

관련 클래스를 추가하기 위해서는 cosine.schema(종속성), inetorgperson.schema(실제 클래스가 있는 스키마) 스키마 파일을 include 하면 되는데 '/private/etc/openldap/schema' 경로에 해당 스키마가 있다.

아래와 같이 ldap 설정 파일에서 스키마 include 를 정의 한다.

$ sudo vi slapd.conf
include     /private/etc/openldap/schema/core.schema
include     /private/etc/openldap/schema/cosine.schema
include     /private/etc/openldap/schema/inetorgperson.schema

 

적용을 위해 ldap 을 재기동 한다.

이제 유저를 만드는 방법은 위에 글에서 설명한 방식과 다르므로 아래와 같이 진행한다.

context entry 사용시 dn을 마음대로 정의할 수 있다.

그다음 사용자 클래스를 선택한뒤 'Add' 를 눌러 추가한다.

그럼 의존되어 있는 class 도 자동으로 추가된다.

그 다음 아래와 같이 DN 을 정의한다.

next 를 누르면 해당 클래스에서 필수로 필요로 하는 값이 빨간색으로 표기되어 있다.

아래와 같이 모두 입력하고 추가 속성을 넣기 위해 오른쪽 상단에 + 버튼을 클릭한다.

패스워드 설정을 위해 userPassword 를 검색하여 선택하고 Finish 를 누른다.

원하는 패스워드와 암호방식 선택한뒤 ok를 누른다.

이제 Finish 를 눌러 유저 추가를 완료 한다.

아래와 같이 정상적으로 사용자가 추가 됐음을 볼 수 있다.

이제 기존 임베디드 ldap 사용하던 코드를 수정해보자

build.gradle 파일에서 임베디드 라이브러리를 주석처리 한다.

dependencies {
    ...
    /*implementation 'com.unboundid:unboundid-ldapsdk'*/ // In-Memory Directory Server
    ...
}

그 다음 ldap 설정 파일인 LdapWebSecurityConfig을 보면 구성 부분은 같게 하여 수정할 부분은 없으며 port 만 구축한 ldap 서버 포트인 389로 수정한다.

@Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception
    {
        auth.ldapAuthentication()
                ...
                .url("ldap://localhost:389/dc=corin,dc=com")
                ...
    }

그리고 application.yml 에 정의한 embedded 설정을 모두 지운다. 지울 코드는 아래와 같으며 모두 제거 한다.

spring:
  ldap:
    embedded:
      ldif: classpath:ldap-server.ldif
      base-dn: dc=corin,dc=com
      port: 8389

이제 어플리케이션을 실행시키고 메인 페이지  'http://localhost:8080' 에 접속한다.

비밀번호 틀릴시 아래와 같이 나온다. (spring security 기본 화면)

정상적으로 입력시 아래와 같이 문구가 정상적으로 나온다.

이제 ldap 서버에서 sn(성), cn(이름), 그룹 등 속성 정보를 가져와서 로그인 완료시 띄워주도록 하는 실습을 진행해본다.

그 전에 아래와 같이 그룹, 유저를 생성한다.

DN class attributes
ou=groups,dc=corin,dc=com organizationalUnit ou=groups
cn=architectures,ou=groups,dc=corin,dc=com groupOfUniqueNames  cn=architectures
ou=architecture
uniqueMember=uid=corin,ou=people,dc=corin,dc=com
cn=developers,ou=groups,dc=corin,dc=com groupOfUniqueNames cn=developers
ou=developer
uniqueMember=uid=corin,ou=people,dc=corin,dc=com
uniqueMember=uid=piano,ou=people,dc=corin,dc=com
ou=people,dc=corin,dc=com organizationalUnit ou=people
uid=corin,ou=people,dc=corin,dc=com inetOrgPerson cn=yoolee
sn=hong
uid=corin
userPassword=[원하는 암호로 설정]
uid=piano,ou=people,dc=corin,dc=com inetOrgPerson cn=piano
sn=kim
uid=piano
userPassword=[원하는 암호로 설정]

모두 생성하면 전체 구조는 아래와 같다.

이제 유저 정보를 추가적으로 셋팅하기 위해서 유저 정보를 매핑하는 클래스(LdapUserDetailsMapper)를 상속받아 커스텀 하게 만들 것이다.

그 전에 유저정보를 담을 새로운 유저 클래스를 생성하고

@Data
public class LdapUser extends LdapUserDetailsImpl {
    private String dn;
    private String cn;
    private String sn;
    private String uid;
    private List<String> groups;
}

해당 유저가 속한 그룹을 검색하기 위한 ldapContext 를 bean으로 등록한다.

@Slf4j
@Configuration
public class WebConfig {

    @Autowired
    public LdapInfo ldapInfo;

    @Bean
    LdapContext ldapContext(){
        LdapContext ctx = null;
        try{
            Hashtable<String, String> environment = new Hashtable<>();
            environment.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
            environment.put(Context.PROVIDER_URL, ldapInfo.getUrl());
            environment.put(Context.SECURITY_AUTHENTICATION, "simple");
            environment.put(Context.SECURITY_PRINCIPAL, ldapInfo.getManagerdn());
            environment.put(Context.SECURITY_CREDENTIALS, ldapInfo.getManagerpwd());
            ctx = new InitialLdapContext(environment, null);
            log.info("[INFO] ldapContext Connected!");
        }catch (NamingException e){
            log.error("[ERROR] Create LdapContext Exception {}", e);
        }
        return ctx;
    }
}

 추가로 ldap 연결을 위한 properties는 따로 만든 뒤

// ldap.properties

ldap.url=ldap://localhost:389
ldap.basedn=dc=corin,dc=com
ldap.managerdn=cn=Manager,dc=corin,dc=com
ldap.managerpwd=admin@1234
ldap.usersearchPattern=uid={0},ou=people
ldap.groupsearchPattern=(&(objectclass=groupOfUniqueNames)(uniqueMember=uid=%s,ou=people,dc=corin,dc=com))

쉽게 읽어올 수 있는 vo 객체를 만든다.

@Data
@Configuration
@ConfigurationProperties(prefix = "ldap")
@PropertySource(value = "classpath:ldap.properties")
public class LdapInfo {
    private String url;
    private String basedn;
    private String managerdn;
    private String managerpwd;
    private String usersearchPattern;
    private String groupsearchPattern;
}

이제 LdapUserDetailsMapper 클래스를 상속하는 CustomLdapUserMapper 클래스를 만들고

mapUserFromContext 메소드를 오버라이딩 하여 cn, sn 정보를 가져와 LdapUser 객체에 셋팅하고

유저가 속한 그룹의 경우 filter를 통해 ldap에서 조회한 뒤 LdapUser 객체에 셋팅 한다.

@Slf4j
@Configuration
public class CustomLdapUserMapper extends LdapUserDetailsMapper {

    @Autowired
    public LdapContext ldapContext;

    @Autowired
    public LdapInfo ldapInfo;

    @Override
    public LdapUser mapUserFromContext(DirContextOperations ctx, String username, Collection<? extends GrantedAuthority> authorities) {
        LdapUserDetailsImpl details = (LdapUserDetailsImpl) super.mapUserFromContext(ctx, username, authorities);
        LdapUser customLdapUser = new LdapUser(){{
            setCn(ctx.getStringAttribute("cn"));
            setDn(details.getDn());
            setSn(ctx.getStringAttribute("sn"));
            setUid(details.getUsername());
        }};
        try{
            // 유저가 속한 그룹 검색
            SearchControls controls = new SearchControls();
            controls.setReturningAttributes(new String[] {"cn","ou"});
            controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
            String filter = String.format(ldapInfo.getGroupsearchPattern(), details.getUsername());
            NamingEnumeration<?> answer = ldapContext.search(ldapInfo.getBasedn(), filter, controls);

            List<String> groups = new ArrayList<>();
            while(answer.hasMore()) {
                SearchResult searchResult = (SearchResult) answer.next();
                Attributes attributes = searchResult.getAttributes();
                groups.add(StringUtils.isEmpty(attributes.get("ou")) ? "" : attributes.get("cn").toString().split(":")[1]);
            }
            customLdapUser.setGroups(groups);
        }catch (Exception e){
            log.error("[ERROR] {}", e);
        }
        return customLdapUser;
    }
}

그 다음 로그인 성공시에만 유저정보를 담은 LdapUser 객체를 세션에 저장하기 위해

login success handler 를 만들고 세션에 유저정보를 담는 코드를 작성한다.

@Slf4j
@Configuration
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        LdapUser ldapUser = (LdapUser) authentication.getPrincipal();
        HttpSession session = request.getSession();
        session.setAttribute("ldapUser", ldapUser);
        response.sendRedirect("/");
    }
}

로그인 성공시 커스텀하게 만든 handler를 탈 수 있도록 spring security 설정을 한다.

@EnableWebSecurity
public class LdapWebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception
    {
        http.authorizeRequests().anyRequest().fullyAuthenticated()
                .and().formLogin().successHandler(new LoginSuccessHandler());
    }
...
}

마지막으로 controller 에서 session 정보를 가져와 화면에 뿌린다.

@Slf4j
@RestController
public class TestController {

    @GetMapping("/")
    public String index(HttpServletRequest request) {
        LdapUser ldapInfo = (LdapUser) request.getSession().getAttribute("ldapUser");
        return "Hello World "+ ldapInfo.getSn()+ldapInfo.getCn() +" !! [groups] "+ StringUtils.join(ldapInfo.getGroups(),',');
    }
}

이제 테스트를 해보자!

로그인을 하면 아래와 같이 정상적으로 성, 이름, 그룹이 나오는것을 볼 수 있다.

최종 코드는 아래 깃을 참고한다.

:: https://github.com/works-code/ldap-login

 

GitHub - works-code/ldap-login: 직접 구축한 ldap 연동을 위한 스타터 코드 (사용자 성, 이름, 그룹 정보

직접 구축한 ldap 연동을 위한 스타터 코드 (사용자 성, 이름, 그룹 정보 가져오는 코드도 포함) - GitHub - works-code/ldap-login: 직접 구축한 ldap 연동을 위한 스타터 코드 (사용자 성, 이름, 그룹 정보 가

github.com

끝!

728x90
Comments