2006-05-21

NTLM 인증

윈도우 인트라넷 환경에서 NTLM(Windows NT LAN Manager) 프로토콜로 SSO(Single Sign On)를 구현하는 경우가 있다.

자바(JSP)에서 NTLM 프로토콜을 이용해서 윈도우 도메인 이름과 사용자 계정을 획득하는 방법이다.

<%@ page contentType="text/html; charset=UTF-8" %>

<%
String auth = request.getHeader("Authorization");
if (auth == null) {
    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    response.setHeader("WWW-Authenticate", "NTLM");
    response.flushBuffer();
    return;
    // 이 응답을 받은 IE 웹 브라우저는 다시 요청을 보낸다.
}

if (auth.startsWith("NTLM ")) {
    byte[] msg = new sun.misc.BASE64Decoder().decodeBuffer(auth.substring(5));
    int off = 0, length, offset;
    if (msg[8] == 1) {
        byte z = 0;
        byte[] msg1 = { (byte) 'N', (byte) 'T', (byte) 'L',
                        (byte) 'M', (byte) 'S', (byte) 'S', (byte) 'P', z,
                        (byte) 2, z, z, z, z, z, z, z, (byte) 40, z, z, z,
                        (byte) 1, (byte) 130, z, z, z, (byte) 2, (byte) 2,
                        (byte) 2, z, z, z, z, // this line is 'nonce'
                        z, z, z, z, z, z, z, z };
        response.setContentLength(0);
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        String value = "NTLM " + new sun.misc.BASE64Encoder().encodeBuffer(msg1);

        // 웹로직에서는 Header 값에 CRLF 문자를 포함하면 예외가 발생함.
        response.setHeader("WWW-Authenticate", value.replaceAll("\n", ""));
        response.flushBuffer();
        return;
        // 이 응답을 받은 IE 웹 브라우저는 다시 요청을 보낸다.
    } else if (msg[8] == 3) {
        // 최종적으로 윈도우 도메인 이름과 사용자 계정을 도출할 수 있는 요청이다.
        String remoteHost = null;
        String domain = null;
        String username = null;

        off = 30;
        length = msg[off + 17] * 256 + msg[off + 16];
        offset = msg[off + 19] * 256 + msg[off + 18];
        remoteHost = new String(msg, offset, length);

        length = msg[off + 1] * 256 + msg[off];
        offset = msg[off + 3] * 256 + msg[off + 2];
        domain = new String(msg, offset, length);

        length = msg[off + 9] * 256 + msg[off + 8];
        offset = msg[off + 11] * 256 + msg[off + 10];
        username = new String(msg, offset, length);

        // 사용자 계정에 포함된 특수 문자 제거
        username = username.replaceAll("\\W", "");
        return;
    }
}
%>


물론 이 경우에는 MS IE 웹 브라우저만을 사용해야 한다.


참고 자료



2006-05-05

Active Directory 연동

LDAP(Lightweight Directory Access Protocal)과 JNDI(Java Naming and Directory Interface) API를 이용해서 MS Active Directory와 연동(사용자 정보 검색)하는 방법이다.


Active Directory 서버 연결 시작

ActiveDirectoryDao 클래스의 생성자에서 Active Directory 서버와 연결한다.

ActiveDirectoryDao.java

private DirContext ldapContext;

private String host = "127.0.0.1";

private String port = "389";

private String username = "username";

private String password = "password";

public ActiveDirectoryDao() {
    try {
        Hashtable ldapEnv = new Hashtable(5);
        ldapEnv.put(Context.INITIAL_CONTEXT_FACTORY,
                "com.sun.jndi.ldap.LdapCtxFactory");
        ldapEnv.put(Context.PROVIDER_URL, "ldap://" + host + ":" + port);
        ldapEnv.put(Context.SECURITY_PRINCIPAL, username);
        ldapEnv.put(Context.SECURITY_CREDENTIALS, password);
        ldapContext = new InitialDirContext(ldapEnv);
    } catch (NamingException e) {
        throw new SystemException(e);
    }
}


Active Directory 서버 연결 종료

ActiveDirectoryDao 클래스의 close 메소드로 Active Directory 서버와의 연결을 종료한다.

ActiveDirectoryDao.java

public void close() {
   if (ldapContext != null) {
       try {
           ldapContext.close();
       } catch (NamingException e) {
           throw new SystemException(e);
       }
   }
}


사용자 존재 여부 확인

ActiveDirectoryDao 클래스의 hasUser 메소드로 Active Directory 서버에 특정 사용자 정보가 존재하는지 확인한다.

ActiveDirectoryDao.java

private String baseDn = "...";

public boolean hasUser(String name) {
    try {
        NamingEnumeration all = ldapContext.search(baseDn, "name=" + name,
                getSearchControl());
        return all.hasMoreElements();
    } catch (NamingException e) {
        throw new SystemException(e);
    }
}


하위 노드에서도 사용자 정보를 검색하도록 SearchControls를 설정한다. 사용자 아이디가 유일해야 한다.

ActiveDirectoryDao.java

protected SearchControls getSearchControl() {
    SearchControls result = new SearchControls();
    result.setCountLimit(1);
    result.setSearchScope(SearchControls.SUBTREE_SCOPE);
    return result;
}


사용자 정보 확인

ActiveDirectoryDao 클래스의 getUser 메소드로 Active Directory 서버에 있는 특정 사용자 정보를 java.util.Map 객체로 반환한다. 사용자가 존재하지 않으면 java.util.Collections.EMPTY_MAP 객체를 반환한다.


ActiveDirectoryDao.java

public Map getUser(String name) {
    try {
        NamingEnumeration all = ldapContext.search(baseDn, "name=" + name,
                getSearchControl());
        while (all.hasMoreElements()) {
            SearchResult each = (SearchResult) all.nextElement();
            Map result = new HashMap();
            NamingEnumeration attributes = each.getAttributes().getAll();
            while (attributes.hasMoreElements()) {
                Attribute attribute = (Attribute) attributes.nextElement();
                result.put(attribute.getID(), attribute.get());
            }
            return result;
        }
        return Collections.EMPTY_MAP;
    } catch (NamingException e) {
        throw new SystemException(e);
    }
}


사용 예제

아래처럼 ActiveDirectoryDao 클래스를 사용한다.

Client.java

ActiveDirectoryDao dao = null;

try {
    dao = new ActiveDirectoryDao();
    Map userInfo = null;
    if (dao.hasUser("username")) {
        userInfo = dao.getUser("username");
    }
} finally {
    if (dao != null) {
        dao.close();
    }
}

Active Directory 서버와의 연결을 유지하려면 close 메소드를 호출하지 않는다.


참고 자료



* ActiveDirectoryDao 전체 소스 코드

package com.dimdol.example;

import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;

/**
 * JNDI/LDAP을 이용해서 MS Active Directory와 연동하는 클래스.
 *
 * @version 1.0
 */
public class ActiveDirectoryDao {

    /**
     * 기본 루트.
     */
    private DirContext ldapContext;

    /**
     * Active Directory 서버의 호스트.
     */
    private String host = "active.dimdol.com";

    /**
     * Active Directory 서버의 포트.
     */
    private String port = "389";

    /**
     * 사용자.
     */
    private String username = "";

    /**
     * 패스워드.
     */
    private String password = "";

    /**
     * 기본 Distinguished Name,
     */
    private String baseDn = "OU=조직이름,DC=corp,DC=dimdol,DC=com";

    /**
     * 기본 생성자. Active Directory 서버와 연결한다.
     */   
    public ActiveDirectoryDao(String username, String password) {
        try {
            Hashtable ldapEnv = new Hashtable(5);
            ldapEnv.put(Context.INITIAL_CONTEXT_FACTORY,
                    "com.sun.jndi.ldap.LdapCtxFactory");
            ldapEnv.put(Context.PROVIDER_URL, "ldap://" + host + ":" + port);
            ldapEnv.put(Context.SECURITY_PRINCIPAL, username + "@corp.dimdol.com");
            ldapEnv.put(Context.SECURITY_CREDENTIALS, password);
            ldapContext = new InitialDirContext(ldapEnv);
        } catch (NamingException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Active Directory 서버와의 연결을 종료한다.
     */
    public void close() {
        if (ldapContext != null) {
            try {
                ldapContext.close();
            } catch (NamingException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 사용자가 존재하는지 확인한다.
     *
     * @param name
     *            사용자 아이디
     * @return 사용자가 존재하면 true를, 존재하지 않으면 false를 반환한다.
     */
    public boolean hasUser(String name) {
        try {
            NamingEnumeration all = ldapContext.search(baseDn, "name=" + name,
                    getSearchControl());
            return all.hasMoreElements();
        } catch (NamingException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 사용자 정보를 java.util.Map 객체에 담아서 반환한다. 사용자가 존재하지 않으면
     * java.util.Collections.EMPTY_MAP 객체를 반환한다.
     *
     * @param name
     *            사용자 아이디
     * @return 사용자 정보를 담고 있는 java.util.Map 객체
     */
    public Map getUser(String name) {
        try {
            NamingEnumeration all = ldapContext.search(baseDn, "name=" + name,
                    getSearchControl());
            while (all.hasMoreElements()) {
                SearchResult each = (SearchResult) all.nextElement();
                Map result = new HashMap();
                NamingEnumeration attributes = each.getAttributes().getAll();
                while (attributes.hasMoreElements()) {
                    Attribute attribute = (Attribute) attributes.nextElement();
                    result.put(attribute.getID(), attribute.get());
                }
                return result;
            }
            return Collections.EMPTY_MAP;
        } catch (NamingException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 하위 노드에서도 검색하도록 설정한 SearchControls를 반환한다.
     *
     * @return 하위 노드에서도 검색하도록 설정한 SearchControls
     */
    protected SearchControls getSearchControl() {
        SearchControls result = new SearchControls();
        result.setCountLimit(1);
        result.setSearchScope(SearchControls.SUBTREE_SCOPE);
        return result;
    }

    public static void main(String[] args) throws Exception {
        ActiveDirectoryDao dao = new ActiveDirectoryDao("","");
        System.out.println(dao.hasUser("dimdol"));
        System.out.println(dao.getUser("dimdol").get("description"));
        dao.close();
    }

}