Acceso a LDAP mediante Netscape Directory SDK
viernes, mayo 11, 2007 at 2:00AM Acceso a OpenLDAP mediante Netscape Directory SDK para Java vía JNDI en una aplicación web con Apache Tomcat.
Fecha de creación: 04.05.2007
Revisión 0.1 (04.05.2007)
Irenio Luis Chagua Aduviri
ichagua@nspsac.com
|
Introducción
Cuando se está desarrollando aplicaciones empresariales, muchas veces se tiene un directorio de personas, objetos, que son necesarios tenerlos ordenado en un formato estándar, uno de ellos es LDAP (Lightweight Directory Access Protocol), pero las formas de acceso a este directorio son diversas, por eso se ha pensado en implementar el acceso desde un directorio SDK para Java utilizando un servidor web Apache Tomcat. El directorio LDAP utilizado es OpenLDAP, uno de los directorios que está al alcance del mundo del software libre.
En este artículo se presenta el uso del directorio LDAP con la finalidad de conocer con mayor profundidad, creando nuevas especificaciones de esquemas del directorio y nuevas clases de objetos con sus respectivos atributos. El mismo directorio es posible mostrar mediante un Browser LDAP en modo gráfico.
La aplicación desarrollada empieza con la creación de las entradas del directorio LDAP, configuración de la librería Netscape Directory SDK para Java, configuración de recursos JNDI con las clases JavaBean, implementación de las clases DAO para la aplicación haciendo uso del mismo directorio SDK para la búsqueda, autenticación de usuarios, creación y manipulación de las entradas de directorio y finalmente es mostrado el acceso a LDAP mediante Servlets para enviar datos a una página web haciendo uso de JSP.
Algunos de los pasos de la configuración de las herramientas se han obviado, en caso de no concerlos pueden encontrar información básica en los distintos sitios mencionados en la referencia.
LDAP
LDAP (Lightweight Directory Access Protocol) o Protocolo de Acceso Ligero a Directorio es un protocolo de comunicación que permite acceder y modificar información almacenada en un directorio ordenado y distribuido en forma jerárquica, actualmente está en la versión 3. LDAP permite almacenar información de cuentas de usuario, contactos, ubicación de diversos recursos de la red, permisos de usuarios, certificados, entre otros; éste directorio está optimizado para acceso de lectura en forma eficiente y almacenar datos de poco tamaño, las modificaciones se presentan con poca frecuencia como el caso de un correo electrónico.
El protocolo LDAP accede a la información contenida en un árbol de información de directorio, las cuales están formados por un conjunto de atributos, indicando el nombre y su respectivo valor. Este directorio es único en una organización y es posible acceder de diversas aplicaciones.
Utilidades de LDAP
- Directorios de información, por ejemplo datos de empleados organizados por departamentos (siguiendo la estructura organizativa de la empresa) ó cualquier tipo de páginas amarillas.
- Sistemas de autenticación/autorización centralizada, sistemas donde se guarda gran cantidad de registros y se requiere un uso constante de los mismos, por ejemplo, gestión de cuentas de acceso a una red corporativa, sistema de autenticación para páginas web, sistemas de control de entradas a edificios, oficinas, entre otros.
- Sistemas de correo electrónico.
- Sistemas de alojamiento de páginas web y FTP.
- Sistemas de autenticación basados en RADIUS, para el control de accesos de los usuarios a una red de conexión o ISP.
- Servidores de certificados públicos y llaves de seguridad.
- Autenticación única ó "single sign-on" para la personalización de aplicaciones.
- Perfiles de usuarios centralizados, para permitir itinerancia ó "Roaming".
- Libretas de direcciones compartidas.
Características de LDAP
- Operaciones de lectura muy rápidas. Debido a la naturaleza de los datos almacenados en los directorios las lecturas son más comunes que las escrituras.
- Datos relativamente estáticos. Los datos almacenados en los directorios no suelen actualizarse con mucha frecuencia.
- Entorno distribuido, fácil replicación.
- Estructura jerárquica. Los directorios almacenan la información de forma jerárquica de forma nativa.
- Orientadas a objetos. El directorio representa a elementos y a objetos. Los objetos son creados como entradas, que representan a un conjunto de atributos.
- Esquema estándar. Los directorios utilizan un sistema estándar que pueden usar fácilmente diversas aplicaciones.
- Atributos multi-valor. Los atributos pueden almacenar un valor único o varios.
- Replicación multi-master. Muchos de los servidores LDAP permiten que se realicen escrituras o actualizaciones en múltiples servidores.
Estructura del Arbol LDAP
Entradas
El modelo de información de LDAP está basado en entradas. Una entrada es un conjunto de atributos que tienen un único Nombre Distintivo DN (Distinguished Name) y de forma global. El DN se utiliza para referirse a una entrada sin ambig�edades. Cada atributo de una entrada tiene un tipo y está asociado uno o más valores. Los tipos son normalmente palabras nemotécnicas, como "cn" para common name, o "mail" para una dirección de correo electrónico.
La sintaxis de los atributos depende del tipo de atributo. Por ejemplo, un atributo cn puede tener el valor "Mariano Apaza Quispe". Un atributo mail puede contener un valor "mapaza@nspsac.com". El atributo jpegPhoto contiene una fotografía en formato JPEG.
Atributos
Los datos del directorio son un conjunto de atributos y sus respectivos valores. Por ejemplo el atributo commonName, o cn, se usa para representar el nombre de una persona.
Los datos de una persona que se registra en el directorio se definen mediante el conjunto de atributos que están definidas en las especificaciones de esquemas, en este caso la clase de objetos person.
En una entrada de directorio se presentan atributos obligatorios que deben estar presentes en la clase de objetos y atributos opcionales que no es necesario que esté presente en una entrada de clase de objetos.
Tipos de Atributos
Una definición de tipo de atributo especifica la sintaxis de un atributo y cómo se ordenan y comparan los atributos de ese tipo.
Los tipos de atributos en el directorio forman un árbol de clases. Por ejemplo, el tipo de atributo "commonName" es una subclase del tipo de atributo "name". Hay atributos obligatorios y opcionales que se muestran en la Tabla 1.1.
|
|
| NUMERICOID (obligatorio) | Identificador de Objeto único (OID) |
| NAME | Nombre del Atributo |
| DESC | Descripción del Atributo |
| OBSOLETE | "true" si es obsoleto; "false" o ausente si no lo es |
| SUP | Nombre del tipo de atributo superior del que se deriva el tipo de atributo |
| EQUALITY | Nombre ó OID de la regla de correspondencia si la igualdad de correspondencia está permitida; ausente si no lo está |
| ORDERING | Nombre o OID de la regla de correspondencia si está permitida la ordenación; ausente si no lo está. |
| SUBSTRING | Nombre o OID de la regla de correspondencia si está permitida la correspondencia de sub-string ausente si no lo está. |
| SYNTAX | "true" si el atributo no es multi-valor; "false" o ausente si lo es |
| COLLECTIVE | "true" si el atributo es colectivo; "false" o ausente si no lo es |
| NO-USER-MODIFICATION | "true" si el atributo no es modificable por el usuario; "false" o ausente si lo es |
| USAGE | Descripción del uso del atributo |
| Tabla 1: RFC 2252: AttributeTypeDescription |
Clase de Objetos
En LDAP, una clase de objetos define el conjunto de atributos a ser usados para definir una entrada. El estándar LDAP proporciona estos tipos básicos para las clases de objetos:
- Grupos en el directorio, entre ellos listas no ordenadas de objetos individuales o de grupos de objetos.
- Emplazamientos, como por ejemplo el nombre del país y su descripción.
- Organizaciones que están en el directorio.
- Personas que están en el directorio.
Por ejemplo, la entrada para personas se define mediante la clase de objetos
person, pero también puede definirse mediante atributos en las clases de objetos inetOrgPerson, groupOfNames y organization La estructura de clases de objetos del servidor determina la lista total de atributos requeridos y permitidos para una entrada concreta.LDIF
El formato de intercambio de LDAP es un archivo de texto que almacena información de entradas de objetos en forma jerárquica. Esto nos permite importar y exportar información de directorio entre servidores de directorios basados en LDAP.
Modelo de Nombres LDAP
Cada entrada de directorio LDAP está organizada en un árbol de información de directorio y para identificar a alguna entrada de directorio se accede mediante su nombre distintivo (DN). Cada nombre distintivo puede estar formado por una secuencia de nombres distintivos relativos RDN (Relative Distinguished Name), como uid=mapaza. Cada nombre distintivo relativo en un DN corresponde a una rama del árbol de información de directorio. Asimismo está formado por otros atributos que lo relacionan con las jerarquías superiores como componentes de dominio DC (Domain Component), así, dc=nspsac, dc=com. Después del nombre distintivo hay una serie de atributos que definen las entradas.
Para comprender un poco sobre la estructura del directorio veamos el árbol de la gráfica donde se describe a la empresa dc=nspsac, dc=com como principal y teniendo como unidades organizacionales a Admin, People y Developer, de las cuales en cada unidad organizacional es posible agregar directorios, en este caso una cuenta de usuario con su información de datos de identificación, registro del empleado, datos de su jefe, correo electrónico, teléfono fijo, teléfono celular, la dirección en la que vive, la clave de identificación de la cuenta y la foto correspondiente del empleado.
![]() |
| Figura 1: Arbol de Directorio LDAP |
mapaza.ldif que se muestra a continuación:
dn: uid=mapaza,ou=People,dc=nspsac,dc=com
givenName: Mariano
sn: Apaza Quispe
mail: mapaza@nspsac.com
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: contribuyente
uid: mapaza
cn: Mariano Apaza Quispe
dni: 01234567
ruc: 10012345675
employeeNumber: A214
manager: uid=jperez,ou=Admin,dc=nspsac,dc=com
telephoneNumber: 051505050
mobile: 0519797979
direccion: Jr. Los Incas Nro. 6548
st: Puno
l: Ilave
userPassword: abc
jpegPhoto:<file:///var/photo.jpg
Además al crear un directorio de entrada los atributos pertenecen a un conjunto de clase de objetos que están definidos en una especificación de esquemas según el estándar, en este caso pertenece a la clase de objetos
person, organizationalPerson, inetOrgPerson y contribuyente, de éste último veremos más adelante.Directorio LDAP
Para almacenar estas descripciones de directorios es necesario el uso de un directorio LDAP, hay varias implementaciones de estos directorios, de distintas empresas, para distintos usos y para aplicaciones diferentes. Para el caso del presente artículo haremos uso de OpenLDAP, un directorio LDAP a nuestro alcance en el mundo del software libre.
Para descargar el instalador de OpenLDAP acceda a http://www.openldap.org/, ó si desea OpenLDAP para Windows. Una vez descargado e instalado OpenLDAP es necesario modificar el archivo de configuración slapd.conf, este archivo contiene configuración de usuario, contraseña, sufijo, dn principal, especificaciones de esquemas, entre otros. En este caso cambiaremos algunos valores de los atributos.
Se ha realizado el cambio del sufijo a "
database bdb
suffix "dc=nspsac,dc=com"
rootdn "cn=root,dc=nspsac,dc=com"
rootpw abc
directory ./data
dc=nspsac,dc=com", y dn principal a "cn=root,dc=nspsac,dc=com", luego la contraseña de la cuenta principal se ha cambiado a abc, pero también es posible colocar una contraseña encriptada con {CRYPT}, {MD5}, {SMD5}, {SSHA}, y {SHA}, si usamos alguna de estas encriptaciones la contraseña de la cuenta principal quedaría como el siguiente:rootpw {SSHA}/Wg8V59/aoeKLn4PkkKWEsdvjyz6R+/EMuchos de los atributos de la entrada de directorio descrito anteriormente no está disponible en la configuración inicial, estos atributos están basados en unas especificaciones de esquemas que delimitan su creación. Para esto quitaremos el comentario ó agregaremos si no está descrito de las siguientes especificaciones de esquemas:Los atributos
include ./schema/core.schema
include ./schema/cosine.schema
include ./schema/inetorgperson.schema
dni, ruc, y direccion no está definido en las especificaciones de esquemas que acabamos de activarlas, estos atributos lo he agregado para fines de conocer cómo se crea una nueva especificación de esquemas y por otro lado en mi país (Perú) todo usuario de un sistema al menos tiene su documento nacional de identidad (DNI), su registro único del contribuyente (RUC) y se agregó el atributo dirección por comodidad, aunque el atributo dni y direccion no debiera estar definido en una clase de objetos como la de contribuyente, más bien debería estar definido en la clase de objetos person, para efectos de no complicar la configuración se ha añadido en la clase de objeto contribuyente.En una especificación de esquema se definen clases de objetos válidos que indican qué atributos debe contener en forma obligatoria y qué atributos son opcionales, así como el tipo de dato (cadenas de texto, números) de un atributo. La clase de objetos y los atributos deben estar definidos en forma global y único mediante cadenas de números (OID), para esto es necesario obtener un OID que nos permitirá crear tantas extensiones como se quiera del esquema.
Los tipos de clase de objetos que existen son tres: Structural Una clase de objeto estructural define las características básicas de un objeto. Auxiliary Una clase de objeto Auxiliar es adicional, complementa los atributos de una clase de objeto estructural. Y por último Abstract Esta clase de objeto abstracto es usado solamente para definir modelo de datos LDAP básicos.
La sintaxis básica para crear una clase de objetos es la siguiente:
objectclass ( 1.1.2.2.2 NAME 'myPerson'Donde:
DESC 'Mi persona'
SUP inetOrgPerson
MUST ( myUniqueName $ givenName )
MAY myPhoto )
1.1.2.2.2 es el identificador único global (OID).
NAME 'myPerson' es el nombre de la clase de objeto (alias para OID).
DESC 'Mi persona' la descripción para la clase de objeto.
SUP inetOrgPerson es el objeto del que hereda.
MUST(...) aquí se describen los atributos requeridos.
MAY(...) aquí se describen los atributos opcionales.
La sintaxis básica para crear un atributo es la siguiente:
attributetype ( 1.3.6.1.1.1.1.0 NAME 'uidNumber'Donde:
DESC 'Identifica en forma única a un usuario'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
1.3.6.1.1.1.1.0 es el identificador único global (OID).
NAME 'uidNumber' es el nombre del atributo (alias para OID).
DESC '...' la descripción para el atributo.
EQUALITY integerMatch calificador de plantilla de tipos.
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 es la sintaxis OID.
SINGLE-VALUE es un calificador, puede ser SINGLE-VALUE, COLLECTIVE y {LENGTH}.
|
|
|
| Binary | 1.3.6.1.4.1.1466.115.121.1.5 | BER/DER data |
| Boolean | 1.3.6.1.4.1.1466.115.121.1.7 | boolean value |
| Distinguished Name | 1.3.6.1.4.1.1466.115.121.1.12 | DN |
| Directory String | 1.3.6.1.4.1.1466.115.121.1.15 | UTF-8 string |
| IA5String | 1.3.6.1.4.1.1466.115.121.1.26 | ASCII string |
| Integer | 1.3.6.1.4.1.1466.115.121.1.27 | Integer |
| Name and Optional UID | 1.3.6.1.4.1.1466.115.121.1.34 | DN plus UID |
| Numeric String | 1.3.6.1.4.1.1466.115.121.1.36 | Numeric String |
| OID | 1.3.6.1.4.1.1466.115.121.1.38 | Object Identifier |
| Octet String | 1.3.6.1.4.1.1466.115.121.1.40 | Arbitrary Octets |
| Printable String | 1.3.6.1.4.1.1466.115.121.1.44 | Printable String |
| Tabla 2: Sintaxis de los atributos |
|
|
|
| booleanMatch | equality | Boolean |
| objectIdentiferMatch | equality | OID |
| distinguishedNameMatch | equality | DN |
| uniqueMemberMatch | equality | DN with optional UID |
| numericStringMatch | equality | numerical |
| numericStringOrdering | ordering | numerical |
| numericStringSubstringsMatch | substrings | numerical |
| caseIgnoreMatch | equality | case insensitive, space insensitive |
| caseIgnoreOrderingMatch | ordering | case insensitive, space insensitive |
| caseIgnoreSubstringsMatch | substrings | case insensitive, space insensitive |
| caseExactMatch | equality | case sensitive, space insensitive |
| caseExactOrderingMatch | ordering | case sensitive, space insensitive |
| caseExactSubstringsMatch | substrings | case sensitive, space insensitive |
| caseIgnoreIA5Match | equality | case insensitive, space insensitive |
| caseIgnoreIA5OrderingMatch | ordering | case insensitive, space insensitive |
| caseIgnoreIA5SubstringsMatch | substrings | case insensitive, space insensitive |
| caseExactIA5Match | equality | case sensitive, space insensitive |
| caseExactIA5OrderingMatch | ordering | case sensitive, space insensitive |
| caseExactIA5SubstringsMatch | substrings | case sensitive, space insensitive |
| Tabla 3: Reglas de plantillas de atributos |
Regresando nuevamente a la clase de objeto contribuyente y con la base de los conceptos para crear las clases de objetos y atributos en una especificación de esquema, se define el esquema en el archivo de texto con el nombre contribuyente.schema en el directorio schema de la ruta donde está instalado OpenLDAP, con el siguiente contenido:
#La especificación de esquema contribuyente debe ser agregado en el archivo de configuración slapd.conf la siguiente línea.
# OID prefix: 1.3.6.1.4.1.10018
#
# Attributes: 1.3.6.1.4.1.10018.1.1
#
attributetype ( 1.3.6.1.4.1.10018.1.1.1 NAME 'ruc'
DESC 'Registro único del Contribuyente'
EQUALITY numericStringMatch
SUBSTR numericStringSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.36{11} )
attributetype ( 1.3.6.1.4.1.10018.1.1.2 NAME 'razonSocial'
DESC 'Razón Social del Contribuyente'
SUP name )
attributetype ( 1.3.6.1.4.1.10018.1.1.3 NAME 'dni'
DESC 'Documento Nacional de Identidad de la Persona'
EQUALITY numericStringMatch
SUBSTR numericStringSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.36{8} )
attributetype ( 1.3.6.1.4.1.10018.1.1.4 NAME 'dniRepLegal'
DESC 'Documento Nacional de Identidad del Representante Legal del contribuyente'
EQUALITY numericStringMatch
SUBSTR numericStringSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.36{8} )
attributetype ( 1.3.6.1.4.1.10018.1.1.5 NAME 'estadoDomicilio'
DESC 'Estado del Domicilio del contribuyente'
SUP name )
attributetype ( 1.3.6.1.4.1.10018.1.1.6 NAME 'direccion'
DESC 'Domicilio de la Persona'
SUP name )
#
# Objects: 1.3.6.1.4.1.10018.1.2
#
objectclass ( 1.3.6.1.4.1.10018.1.2.1 NAME 'contribuyente'
DESC 'contribuyente'
SUP top AUXILIARY
MUST ( ruc $ dni )
MAY ( dniRepLegal $ estadoDomicilio $ razonSocial $ direccion) )
include ./schema/contribuyente.schemaUna vez creado el archivo y guardado reiniciamos nuestro servicio de directorio OpenLDAP.
Antes de continuar con agregar el directorio definido anteriormente en el fichero mapaza.ldif es necesario crear las unidades organizacionales, tal como se ha visto en el árbol LDAP del gráfico, sino creamos estas unidades organizacionales no es posible agregar esta entrada de directorio. Para esto creamos un archivo de texto con el nombre ounspsac.ldif con el siguiente contenido.
dn: ou=Admin,dc=nspsac,dc=comPara agregar un directorio de entrada LDAP en formato LDIF ejecutaremos el siguiente comando.
objectClass: top
objectClass: organizationalUnit
ou: Admin
dn: ou=People,dc=nspsac,dc=com
objectClass: top
objectClass: organizationalUnit
ou: People
dn: ou=Developer,dc=nspsac,dc=com
objectClass: top
objectClass: organizationalUnit
ou: Developer
slapadd -v -f slapd.conf -l ounspsac.ldifDe otra forma también es posible insertar los datos de entrada del directorio con el siguiente comando.
ldapadd -x -D "cn=root,dc=nspsac,dc=com" -w abc -f ounspsac.ldifDe modo similar ahora añadimos al directorio LDAP lo que habíamos definido inicialmente.
slapadd -v -f slapd.conf -l mapaza.ldifY así añadimos otras entradas de directorio LDAP, pero ahora la contraseña del usuario será encriptado con
{SSHA} y es de la siguiente manera.dn: uid=jmamani,ou=People,dc=nspsac,dc=comPara generar una cadena de texto encriptada se hace uso del comando
givenName: Juan Antonio
sn: Mamani Choque
mail: jmamani@nspsac.com
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: contribuyente
uid: jmamani
cn: Juan Antonio Mamani Choque
dni: 01847569
ruc: 10018475691
employeeNumber: 2097
mobile: 0519535353
direccion: Av. Titicaca Nro. 458
st: Puno
l: Juli
userPassword: {SSHA}IabIHNeVpLkbzDxCANEMj47OJ9QRh9Gj
slappasswd que trae OpenLDAP de la siguiente manera:slappasswd -h {SSHA} -s abcY continuando con agregar una entrada de directorio LDAP.slapadd -v -f slapd.conf -l jmamani.ldifComo se ha visto se ha tenido que agregar en tres veces las entradas de directorio LDAP definidos, pero no es necesario realizar para cada entrada de directorio, es posible juntar las tres entradas de directorio en un solo formato LDIF denominado
nspsac.ldif.Luego de haber insertado datos de entrada de directorio podemos hacer uso del comando slapcat para mostrar todas las entradas de directorio que se encuentra en nuestro directorio LDAP en formato LDIF, esto mismo nos puede servir para guardar estas entradas para lo que sea necesario.
Si se desea realizar una búsqueda mediante un DN es mediante el comando ldapsearch.
ldapsearch -x -b "uid=jmamani,ou=People,dc=nspsac,dc=com"Asimismo es posible eliminar entradas de directorio LDAP mediante el comando
ldapdelete.ldapdelete -x -D "cn=root,dc=nspsac,dc=com" -w abc "uid=jmamani,ou=People,dc=nspsac,dc=com"
Browser LDAP
Hasta aquí hemos visto las operaciones de entradas de directorio OpenLDAP desde líneas de comando, se puede utilizar en modo gráfico mediante un navegador de LDAP, para esto utilizaremos LDAPBrowser que está basado en Java y ejecutamos la shell lbe.bat y configuramos como la que se muestra en la figura. Con esto es posible administrar un directorio LDAP en modo gráfico.
![]() |
| Figura 2: Configuración LDAP Browser |
Librería para acceder a LDAP
Lo anterior ha sido una descripción breve sobre el manejo de un directorio LDAP, pero cuando se quiere incluir el uso de directorios LDAP en aplicaciones empresariales es necesario utilizar una librería para acceder a la información de los directorios, como un lenguaje de programación. Para el presente artículo haremos uso de una librería desarrollada por Netscape, un directorio SDK para Java que también está a nuestro alcance en el mundo de software libre. Una vez descargado el código fuente y compilado según las instrucciones de cómo generar la librería de Netscape, que también pueden bajárselo una de las librerías que he compilado ldapjdk.jar será posible integrar en las aplicaciones empresariales el acceso a un directorio LDAP.
Para realizar búsquedas y localizar información en diversos sistemas como directorios LDAP es necesario una interfaz de múltiples servicios de directorio y de nombres como JNDI (Java Naming Directory Interface). JNDI nos permitirá interactuar desde Java con OpenLDAP, esto mediante un proveedor de servicios de interfaz SPI (Service Provider Interface), que en este caso es el mismo Netscape que ha desarrollado junto a la librería para el acceso a LDAP, con el nombre Service Provider LDAP que también pueden bajárselo ldapsp.jar.
Los archivos generados, una vez compilado el código fuente, específicamente del directorio packages, es necesario que los archivos JAR sean agregados a la variable de entorno CLASSPATH del sistema operativo, asumiendo que en windows se ha creado en el directorio c:\netscape\ldapjava y en FreeBSD ó UNIX en el direcotorio /usr/netscape/ldapjava, que lo llamaremos a esta ruta de directorio como <LDAPSDKHOME> Mediante los siguientes comandos agregamos a la variable de entorno.
En Linux
CLASSPATH=<LDAPSDKHOME>/packages/ldapjdk.jar:<LDAPSDKHOME>/
packages/ldapsp.jar:$CLASSPATH
export CLASSPATH
En FreeBSD, Unix
setenv CLASSPATH <LDAPSDKHOME>/packages/ldapjdk.jar:<LDAPSDKHOME>/
packages/ldapsp.jar:$CLASSPATH
En Windows
set CLASSPATH=<LDAPSDKHOME>/packages/ldapjdk.jar;<LDAPSDKHOME>/
packages/ldapsp.jar;%CLASSPATH%
API JNDI
Mediante el API de JNDI es posible escribir cualquier tipo de programa para acceder a información en directorios LDAP, gestores de Base de datos relacionales, servicios CORBA (COS, Corba Object Service), NDS de Novell, entre otras aplicaciones. Para que un programa de Java busque información de cualquier tipo en un directorio LDAP debe indicarse dentro del programa la ubicación del directorio LDAP mediante un Naming Manager para la ubicación física del sistema. Esto es importante ya que en cualquier momento es posible cambiar el servidor físico del directorio LDAP y no será necesario la modificación de programa fuente para luego compilarlo, sino basta con cambiar los parámetros de configuración.
![]() |
| Figura 3: API JNDI |
Configuración de Recursos JNDI
Para realizar una consulta a una entrada de directorio LDAP mediante la librería Netscape Directory SDK para Java, será necesario la configuración de JNDI en un servidor web, para este artículo lo voy a desarrollar en una aplicación web con Apache Tomcat. El cual trae una implementación JNDI InitialContext para cada instancia de aplicación web que se encuentre ejecutando bajo este servidor. Para esto será necesario que descarguen el instalador de Apache Tomcat y configuralo. El servidor Tomcat es una aplicación web basada en Java creada para ejecutar servlets y páginas JSP que nos ayudará para este propósito.
Para este caso se creará una aplicación web con una estructura de directorios básico, lo nombraremos ldap que se encontrará en el directorio $CATALINA_HOME/webapps Luego es necesario incluir un descriptor de la aplicación que es el archivo web.xml que contendrá la configuración de la aplicación web y estará dentro del directorio WEB-INF/ del directorio raíz de la aplicación creada, es decir $CATALINA_HOME/webapps/ldap/WEB-INF/web.xml
Las entradas InitialContext en una aplicación web son configurados en un elemento <Context> que puede estar definido en $CATALINA_HOME/conf/server.xml ó de preferencia el archivo XML del contexto de la aplicación dentro de META-INF/context.xml
Los recursos definidos en estos elementos pueden estar referidos por los siguientes elementos al utilizar la descripción de una aplicación web que se encuentra en /WEB-INF/web.xml:
- <env-entry> - Es la entrada del entorno de aplicación, un parámetro de valor simple, puede ser usado para configurar de cómo la aplicación funcionará.
- <resource-ref> - Es la referencia del recurso de la aplicación, que es un objeto Factory para recursos como JDBC DataSource, JavaMail Session ó un objeto Factory personalizado.
- <resource-env-ref> - Es el Recurso de referencia del entorno de aplicación, es una nueva variación del elemento <resource-ref> añadido en Servlet 2.4 que es más simple de configurar para recursos que no necesitan información de autenticación.
Para el caso de nuestra aplicación definiremos el elemento
<resource-env-ref> en el descriptor de la aplicación web.xml con el siguiente contenido.
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<resource-env-ref>
<description>
Objeto Factory para una instancia de BeanLDAPHost.
</description>
<resource-env-ref-name>
beanldaphost
</resource-env-ref-name>
<resource-env-ref-type>
com.nspsac.bean.BeanLDAPHost
</resource-env-ref-type>
</resource-env-ref>
</web-app>
Como habrán notado en este descriptor de la aplicación hacemos referencia a un recurso de la aplicación con el nombre
beanldaphost que es de tipo de clase JavaBean com.nspsac.bean.BeanLDAPHost, que luego se implementará.
Cada recurso JNDI disponible es configurado en base a una inclusión de los siguientes elementos dentro de un elemento <Context> ó un elemento <DefaultContext>:
- <Environment> - Configura nombres y valores de entradas de entorno de aplicación que estará en funcionamiento a través de JNDI InitialContext.
- <Resource> - Configura el nombre y el tipo de dato de un recurso disponible para la aplicación.
- <ResourceLink> - Agrega un enlace a un recurso definido en el contexto JNDI global.
Para configurar el recurso JNDI en nuestra aplicación agregamos el
<Context> en el archivo de configuración $CATALINA_HOME/conf/server.xml que es el recurso que hacemos referencia con el nombre beanldaphost.
<Context path="ldap" docBase="ldap" reloadable="true" override="true">
<Resource name="beanldaphost" auth="Container"
type="com.nspsac.bean.BeanLDAPHost"
factory="com.nspsac.bean.BeanLDAPHostFactory"
iphost="localhost"
puerto="389"
dnbase="dc=nspsac,dc=com"
dnmgr="cn=root"
dnpwd="abc"
dnraiz="ou=People"
ctxfactory="com.netscape.jndi.ldap.LdapContextFactory"/>
<Context>
En este recurso JNDI estará almacenado la información necesaria para acceder a un directorio LDAP desde una aplicación web; como habrán notado, este recurso utiliza una clase JavaBean y el Resource Factory que es una clase también JavaBean asociado mediante el atributo
factory con el valor com.nspsac.bean.BeanLDAPHostFactory, y los demás atributos (iphost, puerto, dnbase, dnmgr, dnpwd y dnraiz) de este elemento <Resource> son información que utilizaremos dentro de las clases JavaBean para realizar la conexión hacia el directorio LDAP definido anteriormente. Adicionalmente, podemos apreciar el atributo ctxfactory asociado a un proveedor de servicios de interfaz (SPI) tal como habíamos indicado anteriormente.En esta configuración el atributo factory hace referencia a un Resource Factory, pero según la configuración de tomcat podemos incluir de la siguiente forma "org.apache.naming.factory.BeanFactory", y por razones de personalizar nuestra aplicación, se ha creado una clase JavaBean propio.
Creando las clases JavaBean
Según la configuración JNDI que se ha definido en el descriptor de la aplicación y en el Resource Factory, ahora implementamos nuestra clase JavaBean con el nombre BeanLDAPHost, esto será llamado cada vez que la aplicación se ejecute asociado a Resource Factory.
El Resource Factory predeterminado de Apache a veces puede resultarnos limitado para ciertas operaciones, por tal razón creamos nuestro Resource Factory propio para luego integrarlo en Tomcat, la clase JavaBean Resource Factory personalizado le hemos dado el nombre de
package com.nspsac.bean;
import java.io.Serializable;
import java.util.Hashtable;
public class BeanLDAPHost implements Serializable
{
private Hashtable htLdap;
public BeanLDAPHost(String iphost, int puerto,
String dnmgr, String dnpwd,
String dnbase, String dnraiz,
String ctxfactory)
{
this(iphost, Integer.toString(puerto),
dnmgr, dnpwd, dnbase, dnraiz, ctxfactory);
}
public BeanLDAPHost(String iphost, String puerto,
String dnmgr, String dnpwd,
String dnbase, String dnraiz,
String ctxfactory)
{
htLdap = new Hashtable();
htLdap.put("ldap_iphost", iphost);
htLdap.put("ldap_puerto", puerto);
htLdap.put("ldap_dnmgr", dnmgr);
htLdap.put("ldap_dnpwd", dnpwd);
htLdap.put("ldap_dnbase", dnbase);
htLdap.put("ldap_dnraiz", dnraiz);
htLdap.put("ldap_ctxfactory", ctxfactory);
}
public String getAttribute(String name) throws Exception
{
if(name == null || name.equals(""))
throw new Exception();
else
return (String)htLdap.get(name);
}
}
BeanLDAPHostFactory.Para escribir la clase Resource Factory se debe implementar la interfaz del proveedor de servicio JNDI javax.naming.spi.ObjectFactory. Cada vez que la aplicación web llama al método lookup() en una entrada del contexto que está asociado a este Factory, el método getObjectInstance() es invocado con los siguientes argumentos:
- Object obj - El objeto que contiene la localización ó referencia de la información que puede ser usado en la creación del objeto.
- Name name - Es el nombre al que este Factory está relacionado a nameCtx, ó null si el nombre no está especificado.
- Context nameCtx - El contexto relacionado al que el nombre del parámetro es especificado, ó null si el nombre está relacionado al contexto inicial por defecto.
- Hashtable environment - Es el entorno que es utilizado en la creación de este objeto.
Para compilar las clases JavaBean que hemos creado es necesario que la librería que estamos utilizando del proveedor de servicios de interfaz de Netscape Directory SDK para Java ldapsp.jar esté incluido dentro del directorio de librerías de Tomcat que es
package com.nspsac.bean;
import java.util.Enumeration;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.NamingException;
import javax.naming.RefAddr;
import javax.naming.Reference;
import javax.naming.spi.ObjectFactory;
public class BeanLDAPHostFactory implements ObjectFactory {
public Object getObjectInstance(Object obj,
Name name, Context nameCtx, Hashtable environment)
throws NamingException {
BeanLDAPHost bean = null;
Reference ref = (Reference) obj;
Enumeration addrs = ref.getAll();
String iphost = null;
int puerto = -1;
String dnbase = null;
String dnmgr = null;
String dnpwd = null;
String dnraiz = null;
String ctxfactory = null;
while (addrs.hasMoreElements()) {
RefAddr addr = (RefAddr) addrs.nextElement();
String nombre = addr.getType();
String value = (String) addr.getContent();
if (nombre.equals("iphost")) {
iphost = value;
} else if (nombre.equals("puerto")) {
try {
puerto = Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new NamingException("Invalid 'port' value " + value);
}
} else if(nombre.equals("dnbase")){
dnbase = value;
} else if(nombre.equals("dnmgr")){
dnmgr = value;
} else if(nombre.equals("dnpwd")){
dnpwd = value;
} else if(nombre.equals("dnraiz")){
dnraiz = value;
} else if(nombre.equals("ctxfactory")){
ctxfactory = value;
}
}
bean = new BeanLDAPHost(iphost,puerto,dnmgr,dnpwd,dnbase,dnraiz,ctxfactory);
return (bean);
}
}
$CATALINA_HOME/common/lib. Adicionalmente la librería SDK ldapjdk.jar también deberá estar incluido en este directorio de librerías de Tomcat para que funcione nuestros ejemplos de este artículo.Implementando las clases DAO
Para implementar los accesos a un directorio LDAP mediante una aplicación web utilizaremos el modelo DAO, que es utilizado para separar las operaciones de los datos de bajo nivel desde un nivel más alto de la lógica de negocio. Una aplicación DAO tiene los siguientes componentes:
- Una clase DAO factory
- Una interfaz DAO
- Una clase concreta que implemente la interfaz DAO
- Transferencia de objetos de datos
La interfaz DAO
package com.nspsac.seguridad.ldap;
import java.util.HashMap;
import javax.naming.NamingException;
import com.nspsac.utils.exception.IncompleteConversationalState;
public interface AccesoLDAP{
static final String DIR_IMGS = "../webapps/ldap/imgs/";
static final String[] ATTRS = {"dn","mail","uid","cn",
"dni","ruc","employeeNumber","jpegPhoto","manager","givenName",
"telephoneNumber","mobile","direccion","sn","st","l"};
static final String OBECT_CLASS[] = {
"top", "person", "organizationalPerson",
"inetOrgPerson", "contribuyente"};
public void getInstance() throws NamingException;
public void cambiarClave(String uid, String claveanterior, String clave)
throws NamingException;
public void addEntry(String uid, HashMap attrs);
public void renameEntry(String uid, String newUid);
public void renameEntry(String uid, String newUid, String newGroup);
public void deleteEntry(String uid);
public void addAttribute(String uid, String nameAttr, String valAttr)
throws NamingException;
public void modifyAttribute(String uid, String nameAttr, String valAttr)
throws NamingException;
public void deleteAttribute(String uid, String nameAttr)
throws NamingException;
public HashMap findByEmployeeNumber(String codPers)
throws IncompleteConversationalState;
public HashMap findByUID(String uid)
throws IncompleteConversationalState;
public HashMap autenticar(String uid, String pwd)
throws NamingException, IncompleteConversationalState;
}
La clase DAO Factory
package com.nspsac.seguridad.ldap;
import javax.naming.NamingException;
import com.nspsac.seguridad.ldap.AccesoLDAP;
import com.nspsac.seguridad.ldap.AccesoLDAPImpl;
public class AccesoLDAPFactory {
public static AccesoLDAP create() throws NamingException {
return new AccesoLDAPImpl();
}
}
Y la implementación de la interfaz DAO
La clase InitialContext es configurado como una aplicación web que es desplegado inicialmente, y está disponible para los componentes de la aplicación web (para acceso de solo lectura). Todas las entradas y recursos configurados están asociados al espacio de nombre JNDI java:comp/env para su acceso a un recurso. Para el caso de nuestra clase JavaBean estaría configurado como la que se muestra en el código siguiente.
El método
package com.nspsac.seguridad.ldap;
import java.io.OutputStream;
import java.io.FileOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import netscape.ldap.LDAPAttribute;
import netscape.ldap.LDAPAttributeSet;
import netscape.ldap.LDAPConnection;
import netscape.ldap.LDAPEntry;
import netscape.ldap.LDAPException;
import netscape.ldap.LDAPSearchConstraints;
import netscape.ldap.LDAPSearchResults;
import netscape.ldap.LDAPv3;
import com.nspsac.bean.BeanLDAPHost;
import com.nspsac.utils.exception.IncompleteConversationalState;
public class AccesoLDAPImpl implements AccesoLDAP{
private Hashtable htEnv;
private DirContext nspsacCtx;
public InitialDirContext getInitialDirContext() throws NamingException {
return (InitialDirContext)nspsacCtx;
}
public void getInstance() throws NamingException {
htEnv = getEnvironment();
Hashtable htJndi = new Hashtable();
htJndi.put("java.naming.factory.initial", (String)htEnv.get("DN_CTX"));
htJndi.put("java.naming.provider.url", (String)htEnv.get("DN_HOST_PORT"));
htJndi.put("java.naming.security.principal", (String)htEnv.get("DN_MGR"));
htJndi.put("java.naming.security.credentials", (String)htEnv.get("DN_PWD"));
nspsacCtx = new InitialDirContext(htJndi);
}
public Hashtable getEnvironment() throws NamingException {
InitialContext initCtx = null;
Hashtable htEnv = new Hashtable();
try {
initCtx = new InitialContext();
Context ctx = (Context)initCtx.lookup("java:comp/env");
BeanLDAPHost beanldaphost = (BeanLDAPHost)ctx.lookup("beanldaphost");
String host_port = new StringBuffer("ldap://").append(
beanldaphost.getAttribute("ldap_iphost")).append(":").append(
beanldaphost.getAttribute("ldap_puerto")).toString();
htEnv.put("DN_HOST", beanldaphost.getAttribute("ldap_iphost"));
htEnv.put("DN_PORT", beanldaphost.getAttribute("ldap_puerto"));
htEnv.put("DN_HOST_PORT", host_port);
htEnv.put("DN_MGR", beanldaphost.getAttribute("ldap_dnmgr")+","
+beanldaphost.getAttribute("ldap_dnbase"));
htEnv.put("DN_PWD", beanldaphost.getAttribute("ldap_dnpwd"));
htEnv.put("DN_BASE", beanldaphost.getAttribute("ldap_dnbase"));
htEnv.put("DN_RAIZ", beanldaphost.getAttribute("ldap_dnraiz"));
htEnv.put("DN_CTX", beanldaphost.getAttribute("ldap_ctxfactory"));
ctx.close();
}
catch(Exception e) {
e.printStackTrace();
}
finally {
if(initCtx != null)
initCtx.close();
}
return htEnv;
}
}
getEnvironment recogerá los valores que habíamos definido en nuestra configuración JNDI que son información acerca del directorio LDAP, esto almacenado en un tipo de dato Collection de Java que es Hashtable, para luego estar disponible la conexión hacia el directorio LDAP, el que prepara los datos de conexión hacia este directorio es el método getInstance que tendrá los datos recogidos en dos variables globales de la clase de tipo private para que pueda ser utilizado en otros métodos de la clase.Directorio SDK para Java
Como se ha mencionado anteriormente acerca del acceso a un directorio LDAP desde una aplicación empresarial, aquí implementaremos los métodos para el acceso, tales como autenticación de cuentas de usuario, cambio de atributos, agregar atributos, quitar atributos, buscar mediante un RDN, entre otros.
Un directorio SDK para Java tiene las siguientes funcionalidades:
- Libertad para el manejo de protocolos
- El uso de objetos estándares para devolver y procesar los datos
- Utilidad de clases para el manejo de las entidades específicas de LDAP
- Acceso completo para todos los servicios LDAP
- Modelos de autenticación flexibles
- La habilidad de ejecutar en cualquier parte una vez se haya escrito el código
- Funcionalidad multicapa
- Una plataforma de aplicaciones para directorios
Búsqueda de una Entrada de Directorio
Debido a que la facilidad de un directorio LDAP es su habilidad de mostrar resultados de las consultas en forma rápida, para esto empezaremos con realizar una búsqueda para obtener una entrada de nombre y sus atributos con sus respectivos valores.
Antes de realizar una búsqueda en un directorio LDAP, es necesario tener en cuenta la siguiente información:
- Nombre de Servidor donde está instalado el directorio LDAP, también puede ser la dirección IP del servidor, se usa "localhost" cuando se realizan las pruebas en una sola máquina.
- Número de puerto del directorio LDAP, que es el puerto TCP de la máquina donde el servidor de directorio es escuchado por las conexiones LDAP, el puerto estándar para LDAP es 389 para las conexiones no SSL. Para las conexiones basados en SSL es el puerto 636.
- DN base del árbol de directorio administrado por el servidor, es el nombre base como raíz por donde empezará a realizar la búsqueda, por ejemplo ou=People,dc=nspsac,dc=com.
- Alcance de la búsqueda (Scope), es el punto de partida de una búsqueda y la profundidad a la que realiza la búsqueda en un árbol de directorio, hay tres opciones para este alcance:
- BASE, representado por la constante LDAPConnection.SCOPE_BASE, es usado solo para búsquedas de DN base.
- ONE, representado por la constante LDAPConnection.SCOPE_ONE, es usado para indicar que realice la búsqueda de todas las entradas debajo de DN base, pero no incluye el DN base.
- SUBTREE, representado por la constante LDAPConnection.SCOPE_SUB, es usado para indicar que realice la búsqueda de todas las entradas debajo e incluso el DN base.
"(&(objectclass=person)(uid=" + uid + "))"
- Filtros de búsqueda, es la consulta que se realiza, es usado para filtrar las entradas de directorios y devolver cierta cantidad de registros. Los filtros son usados mediante los paréntesis y combinaciones de los símbolos '&', '|' y '!' que representan 'And', 'Or' y 'Not' respectivamente. Si se quiere ubicar a todas las personas que sus apellidos (representado mediante el atributo sn) empiecen con "Mamani" se realiza mediante el siguiente filtro:
(&(objectclass=person)(sn=Mamani*))
- Los atributos que se quieren mostrar en la búsqueda, puede que a veces solo se requiere algunos atributos y no todos los atributos presentes en una entrada de directorio. Si no se quiere recuperar ningún atributo se puede usar la constante LDAPConnection.NO_ATTRS. Si se desea recibir todos los atributos del usuario se puede usar la constante LDAPConnection.ALL_USER_ATTRS, ó en otro caso puede usar un array de cadenas de caracteres indicando los atributos que se quiere recuperar.
- Opcionalemente las preferencias de búsqueda, estas preferencias incluyen la cantidad de tiempo que se desea permitir para la búsqueda, máximo número de registros que se aceptará, y si la búsqueda debe esperar hasta que todos los datos son recibidos. Las preferencias de búsqueda son especificados usando la clase LDAPSearchConstraints. Los métodos usados en esta clase son las siguientes:
setBatchSize especifica cómo deben ser devueltos los resultados de la búsqueda. El valor cero '0' indica que debe esperar hasta que todos los resultados sean devueltos, el valor uno '1' devuelve cada resultado como esté disponible.
setHopLimit especifica cuántas veces deben ser devueltas en una búsqueda de una entrada.
setMaxResults especifica el número máximo de resultados que deben ser devueltas desde una búsqueda. Se usa el valor cero '0' para resultados ilimitados.
setReferrals especifica si el SDK debe ó no seguir las referencias automáticamente.
setServerTimeLimit especifica el número máximo de segundos que debe tardar en entregar los resultados de la búsqueda.
atributo=valor", que nos puede servir para realizar búsquedas por los atributos uid, employeeNumber, ruc, dni, mail, cn, sn y de los demás atributos, siempre y cuando el atributo sea de tipo cadena de texto. El método tiene el nombre de buscarEnLDAP(String uid).Para la búsqueda, ya contamos con información del nombre del servidor, el puerto del servidor, el DN base, los atributos que están definidos en la variable ATTRS, y las preferencias de búsqueda se ha establecido a un número máximo de registro igual a 1, ya que la búsqueda está pensado para los atributos como nombres distintivos relativos y para obtener un conjunto de registros habría que cambiar estas preferencias de búsqueda.
Los resultados de una búsqueda son devueltos como un objeto LDAPSearchResults. Hay dos métodos para realizar el recorrido: nextElement y next, ambos métodos devuelven un objeto que puede ser LDAPEntry, LDAPReferralException ó LDAPException. Para este caso usaremos el método next.
El método next() de LDAPSearchResults devuelve un objeto LDAPEntry. La clase LDAPEntry contiene los siguientes cuatro métodos:
negetDN que devuelve el nombre distintivo completo de una entrada como una cadena de texto (por ejemplo, uid=jmamani, ou=People, dc=nspsac, dc=com).
getAttribute(String name) en este caso el argumento es el nombre del atributo, del que se quiere es su valor respectivo, es devuelto del tipo LDAPAttribute.
getAttributeSet devuelve un objeto LDAPAttributeSet que representa todos los atributos en esta entrada.
toString devuelve la entrada completa, incluye los atributos obtenidos como una cadena de texto.
LDAPAttribute tiene varios métodos obtener el valor de los atributos. Los métodos que normalmente se utilizan en la mayoría de los casos son los siguientes:
getStringValues devuelve de tipo Enumeration los valores para un atributo de tipo cadena de texto.
getByteValues devuelve de tipo Enumeration los valores para un atributo de tipo binario, como el caso de las fotos.
getName devuelve el nombre del atributo.
private HashMap buscarEnLDAP(String uid)
throws IncompleteConversationalState {
HashMap hMap = new HashMap();
LDAPConnection ldap = new LDAPConnection();
try {
ldap.connect(3, (String)htEnv.get("DN_HOST"),
Integer.parseInt((String)htEnv.get("DN_PORT")),
(String)htEnv.get("DN_MGR"), (String)htEnv.get("DN_PWD"));
LDAPSearchConstraints mySearchConstraints = ldap.getSearchConstraints();
mySearchConstraints.setMaxResults(1);
LDAPSearchResults myResults = null;
myResults = ldap.search( new StringBuffer(
(String)htEnv.get("DN_RAIZ")).append(", ").append(
(String)htEnv.get("DN_BASE")).toString(),
LDAPv3.SCOPE_SUB,
"(&(objectclass=person)(" + uid + "))",
ATTRS, false, mySearchConstraints);
if (myResults.hasMoreElements()) {
LDAPEntry myEntry = myResults.next();
hMap.put("dn", myEntry.getDN());
LDAPAttributeSet entryAttrs = myEntry.getAttributeSet();
Enumeration attrsInSet = entryAttrs.getAttributes();
String namePhoto = ((String)myEntry.getAttribute("uid").
getStringValues().nextElement()).concat(".jpg");
while (attrsInSet.hasMoreElements()) {
LDAPAttribute nextAttr = (LDAPAttribute) attrsInSet.nextElement();
String attrName = nextAttr.getName();
if (attrName.trim().equalsIgnoreCase("jpegPhoto")) {
Enumeration valsInAttr = nextAttr.getByteValues();
if (valsInAttr.hasMoreElements()) {
if(savePhoto(namePhoto, (byte[])valsInAttr.nextElement())){
hMap.put(attrName.trim(), namePhoto);
}
else {
hMap = new HashMap();
hMap.put("Mensaje","Error al recuperar Foto");
break;
}
}
}
else{
Enumeration valsInAttr = nextAttr.getStringValues();
if (valsInAttr.hasMoreElements()) {
hMap.put(attrName.trim(), valsInAttr.nextElement());
}
}
}
}
else{
hMap.put("Mensaje","El Usuario indicado no se encuentra registrado.");
}
}
catch (LDAPException e) {
e.printStackTrace();
hMap.put("Mensaje","ERROR:"
+e.getLDAPResultCode()
+"->"
+LDAPException.errorCodeToString(e.getLDAPResultCode()));
}
catch (NumberFormatException e) {
e.printStackTrace();
hMap.put("Mensaje",
"HA OCURRIDO UN PROBLEMA EN LOS PARAMETROS DEL SERVIDOR:"
+ e.getMessage());
}
finally {
try {
if (ldap.isConnected()) {
ldap.disconnect();
}
}
catch (LDAPException ex) {
throw new IncompleteConversationalState(
"NO SE PUDO CERRAR EL ENLACE:"
+ ex.getLDAPResultCode()
+ "->"
+ LDAPException.errorCodeToString(ex.getLDAPResultCode()));
}
}
return hMap;
}
private boolean savePhoto(String namePhoto, byte[] thePhoto){
boolean saved = false;
try{
OutputStream outputStream = new FileOutputStream(DIR_IMGS+namePhoto);
outputStream.write(thePhoto);
outputStream.flush();
outputStream.close();
saved = true;
}
catch (FileNotFoundException e){
e.printStackTrace();
}
catch (IOException e){
e.printStackTrace();
}
return saved;
}
Autenticando Usuarios
Hasta aquí no hemos visto el tema de autenticación a un directorio LDAP. Las conexiones hasta aquí han sido utilizando la cuenta por defecto configurado en el mismo directorio LDAP, más no hemos utilizado la cuenta de usuario y su contraseña. Por ejemplo es posible que en un directorio se quiera restringir el acceso a ciertos atributos, no permitiendo el acceso a algunos atributos como la fotografía de un empleado, que solamente puede tener acceso personal autorizado. Para realizar cambios tales como agregar, modificar, consultar ó eliminar ciertos atributos de una entrada de directorio LDAP, por lo general se debe autenticar.
El protocolo LDAP proporciona una operación para permitir conectar a los clientes para autenticar al servidor. El método más simple de autenticación soportado por el protocolo es un método que al cliente le permite enviar un DN y contraseña al servidor. Para usar el método de autenticación simple, se puede usar mediante el método LDAPConnection.authenticate ó un método LDAPConnection.connect variante que toma una autenticación pasando como parámetros a DN y contraseña. Algunas de las excepciones que se pueden producir son las siguientes:
LDAPException.NO_SUCH_OBJECT. Esta excepción es lanzada si el DN especificado no corresponde a una entrada del directorio.
LDAPException.INVALID_CREDENTIALS. Esta excepción es lanzada si la contraseña especificado no es correcta.
public HashMap autenticar(String uid, String pwd)
throws NamingException, IncompleteConversationalState {
HashMap hMap = new HashMap();
LDAPConnection ldap = new LDAPConnection();
try {
ldap.connect(3, (String)htEnv.get("DN_HOST"),
Integer.parseInt((String)htEnv.get("DN_PORT")),
(String)htEnv.get("DN_MGR"),
(String)htEnv.get("DN_PWD"));
hMap = buscarEnLDAP("uid=" + uid);
if(hMap.get("dn") != null){
ldap.authenticate( (String) hMap.get("dn"), pwd);
}
if(hMap.get("Mensaje") == null){
hMap.put("Mensaje","Usuario ".concat(uid).concat(" autenticado"));
}
}
catch (LDAPException e) {
e.printStackTrace();
hMap = new HashMap();
switch (e.getLDAPResultCode()) {
case LDAPException.NO_SUCH_OBJECT:
hMap.put("Mensaje",uid.concat(": El usuario indicado no existe"));
break;
case LDAPException.INVALID_CREDENTIALS:
hMap.put("Mensaje",uid.concat(": Password invalido"));
break;
default:
hMap.put("Mensaje",uid.concat(
": No se ha podido realizar la autenticacion, error:"
+ e.getLDAPResultCode()));
break;
}
}
catch (NumberFormatException e) {
e.printStackTrace();
hMap = new HashMap();
hMap.put("Mensaje",
"HA OCURRIDO UN PROBLEMA EN LOS PARAMETROS DEL SERVIDOR:"
+ e.getMessage());
}
finally {
try {
if (ldap.isConnected()) {
ldap.disconnect();
}
}
catch (LDAPException ex) {
throw new IncompleteConversationalState(
"NO SE PUDO CERRAR EL ENLACE:"
+ ex.getLDAPResultCode()
+ "->"
+ LDAPException.errorCodeToString(ex.getLDAPResultCode()) + "<br>"
+ ex.getMessage());
}
}
return hMap;
}
Creando y Manipulando entradas de directorio
En una entrada de directorio LDAP es posible realizar las modificaciones de una entrada, tanto en crear una entrada, modificar una entrada con sus respectivos atributos, y eliminar una entrada. En la modificación de entradas es posible el manejo de los atributos como la de añadir atributos, modificar atributos y eliminar atributos. Estas operaciones siempre estarán presentes en un manejo de entrada de directorios LDAP.
Para agregar una nueva entrada de directorio es necesario definir un nombre distintivo DN para la entrada y los atributos para esta entrada. Para agregar una nueva entrada es necesario tener en cuenta los siguientes pasos:
- Crear un objeto LDAPAttribute para cada atributo que forma la entrada.
- Crear un objeto LDAPAttributeSet y utilizar el método add para agregar cada objeto LDAPAttribute del paso 1.
- Crear un objeto LDAPEntry que especifica el nuevo DN y el LDAPAttributeSet del paso 2.
- Invocar al método LDAPConnection.add con el objeto LDAPEntry del paso 3.
addEntry que recibe dos parámetros, el primero es el nombre distintivo identificador de usuario y el segundo un conjunto de atributos en un Collection HashMap.La otra operación con las entradas es la modificación de sus atributos. Para modificar un atributo de una entrada, se ha utilizado el método
public void addEntry(String uid, HashMap attrs) {
LDAPAttributeSet entryAttrs = new LDAPAttributeSet();
LDAPConnection ldap = new LDAPConnection();
try{
ldap.connect(3, (String)htEnv.get("DN_HOST"),
Integer.parseInt((String)htEnv.get("DN_PORT")),
(String)htEnv.get("DN_MGR"), (String)htEnv.get("DN_PWD"));
String dn = new StringBuffer("uid=").append(uid).append(", ").append(
(String)htEnv.get("DN_RAIZ")).append(", ").append(
(String)htEnv.get("DN_BASE")).toString();
for(Iterator it = attrs.keySet().iterator(); it.hasNext(); ){
String name = (String)it.next();
entryAttrs.add( new LDAPAttribute( name, (String)attrs.get(name)));
}
entryAttrs.add( new LDAPAttribute("objectClass", OBECT_CLASS) );
LDAPEntry myEntry = new LDAPEntry( dn, entryAttrs );
ldap.add(myEntry);
}
catch (LDAPException e) {
e.printStackTrace();
}
}
modifyAttributes del objeto DirContext, también es posible modificar un atributo con el uso del objeto LDAPModification y luego invocando al método modify de LDAPConnection. Para modificar los atributos de una entrada es necesario tener en cuenta los siguientes pasos:- Crear un objeto Attribute para un atributo que se desea modificar.
- Crear un conjunto de objetos ModificationItem, puede ser uno solo, especificando DirContext.ADD_ATTRIBUTE, REPLACE_ATTRIBUTE ó REMOVE_ATTRIBUTE para cada objeto Attribute del paso 1.
- Invocar al método modifyAttributes con el DN y el ModificationItem.
Los datos de una entrada deben estar bien guardados en un directorio LDAP. Pero llega un momento en que será necesario borrar una entrada LDAP. Para esto quitar una entrada de un directorio es muy simple. Solamente se debe especificar qué DN se debe quitar e invocar al método
public void addAttribute(String uid, String nameAttr, String valAttr)
throws NamingException {
ModificationItem[] mods = new ModificationItem[1];
Attribute mod = new BasicAttribute(nameAttr, valAttr);
mods[0] = new ModificationItem(DirContext.ADD_ATTRIBUTE, mod);
String dn = new StringBuffer("uid=").append(uid).append(", ").append(
htEnv.get("DN_RAIZ")).append(",").append(
htEnv.get("DN_BASE")).toString();
nspsacCtx.modifyAttributes(dn, mods);
}
public void modifyAttribute(String uid, String nameAttr, String valAttr)
throws NamingException {
ModificationItem[] mods = new ModificationItem[1];
Attribute mod = new BasicAttribute(nameAttr, valAttr);
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod);
String dn = new StringBuffer("uid=").append(uid).append(", ").append(
htEnv.get("DN_RAIZ")).append(",").append(
htEnv.get("DN_BASE")).toString();
nspsacCtx.modifyAttributes(dn, mods);
}
public void deleteAttribute(String uid, String nameAttr)
throws NamingException {
ModificationItem[] mods = new ModificationItem[1];
Attribute mod = new BasicAttribute(nameAttr);
mods[0] = new ModificationItem(DirContext.REMOVE_ATTRIBUTE, mod);
String dn = new StringBuffer("uid=").append(uid).append(", ").append(
htEnv.get("DN_RAIZ")).append(",").append(
htEnv.get("DN_BASE")).toString();
nspsacCtx.modifyAttributes(dn, mods);
}
LDAPConnection.delete.Por último, la operación en una entrada es el renombramiento, es decir modificando el RDN de una entrada de directorio LDAP. Por ejemplo se puede cambiar
public void deleteEntry(String uid) {
LDAPConnection ldap = new LDAPConnection();
try{
ldap.connect(3, (String)htEnv.get("DN_HOST"),
Integer.parseInt((String)htEnv.get("DN_PORT")),
(String)htEnv.get("DN_MGR"), (String)htEnv.get("DN_PWD"));
String dn = new StringBuffer("uid=").append(uid).append(", ").append(
(String)htEnv.get("DN_RAIZ")).append(", ").append(
(String)htEnv.get("DN_BASE")).toString();
ldap.delete(dn);
}
catch (LDAPException e) {
e.printStackTrace();
}
}
uid=pcarrillo del DN ou=People,dc=nspsac,dc=com para tener el uid=pedro.carrillo del DN ou=People,dc=nspsac,dc=com. El método LDAPConnection.rename invoca la operación LDAP para cambiar el RDN de la entrada.No solamente es posible cambiar el RDN en un mismo nivel del árbol del directorio LDAP, hay la posibilidad de mover ó copiar a un RDN a una parte diferente del árbol del directorio LDAP. Como ejemplo veamos al RDN uid=jmamani del DN ou=People,dc=nspsac,dc=com lo movemos ó lo copiamos a otra parte del árbol como RDN uid= juan.mamani y DN ou=Admin,dc=nspsac,dc=com.
public void renameEntry(String uid, String newUid) {
LDAPConnection ldap = new LDAPConnection();
try{
ldap.connect(3, (String)htEnv.get("DN_HOST"),
Integer.parseInt((String)htEnv.get("DN_PORT")),
(String)htEnv.get("DN_MGR"), (String)htEnv.get("DN_PWD"));
String dn = new StringBuffer("uid=").append(uid).append(", ").append(
(String)htEnv.get("DN_RAIZ")).append(", ").append(
(String)htEnv.get("DN_BASE")).toString();
String newRDN = new StringBuffer("uid=").append(newUid).toString();
ldap.rename(dn, newRDN, true);
}
catch (LDAPException e) {
e.printStackTrace();
}
}
public void renameEntry(String uid, String newUid, String newGroup) {
LDAPConnection ldap = new LDAPConnection();
try{
ldap.connect(3, (String)htEnv.get("DN_HOST"),
Integer.parseInt((String)htEnv.get("DN_PORT")),
(String)htEnv.get("DN_MGR"), (String)htEnv.get("DN_PWD"));
String dn = new StringBuffer("uid=").append(uid).append(", ").append(
(String)htEnv.get("DN_RAIZ")).append(", ").append(
(String)htEnv.get("DN_BASE")).toString();
String newRDN = new StringBuffer("uid=").append(newUid).toString();
String newParentDN = new StringBuffer("ou=").append(newGroup).append(", ").append(
(String)htEnv.get("DN_BASE")).toString();
ldap.rename(dn, newRDN, newParentDN, true);
}
catch (LDAPException e) {
e.printStackTrace();
}
}
Servlets, JSP y LDAP
Una vez que se ha realizado con la implementación de las clases para el acceso a un directorio LDAP, será necesario mostrar el resultado del acceso mediante un Servlet para enviar datos a una página web haciendo uso de JSP. Para esto creamos el Servlet ServletGestionLDAP.
package com.nspsac.seguridad;
import java.util.HashMap;
import javax.naming.NamingException;
import javax.servlet.ServletException;
import javax.servlet.ServletConfig;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.nspsac.seguridad.ldap.AccesoLDAP;
import com.nspsac.seguridad.ldap.AccesoLDAPFactory;
import com.nspsac.utils.exception.IncompleteConversationalState;
public class ServletGestionLDAP extends HttpServlet{
public void init(ServletConfig config) throws ServletException {
super.init(config);
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException
{
HashMap hmAut = null;
HashMap hmSearch = null;
String cambioClave = null;
HttpSession session = request.getSession(true);
try{
String accion = request.getParameter("accion");
String opcion = request.getParameter("modo");
AccesoLDAP acceso = AccesoLDAPFactory.create();
acceso.getInstance();
String usuario = request.getParameter("user");
if(accion.equals("autenticacion")){
String clave = request.getParameter("pwd");
hmAut = acceso.autenticar(usuario,clave);
session.setAttribute("hmAut", hmAut);
session.removeAttribute("hmSearch");
session.removeAttribute("cambioClave");
}
else if(accion.equals("busqueda")){
hmSearch = acceso.findByUID(usuario);
session.setAttribute("hmSearch", hmSearch);
session.removeAttribute("hmAut");
session.removeAttribute("cambioClave");
}
else if(accion.equals("cambioclave")){
String claveanterior = request.getParameter("oldpwd");
String clavenueva = request.getParameter("newpwd");
String claveconfirma = request.getParameter("repwd");
cambioClave = usuario.concat(": Usuario indicado no existe.");
if(clavenueva.equals(claveconfirma)){
acceso.cambiarClave(usuario,claveanterior,clavenueva);
cambioClave = usuario.concat(": La clave ha sido clambiada satisfactoriamente.");
}
else{
cambioClave = usuario.concat(": Clave nueva y su confirmación no concuerdan."+
"<br>Ingresar nuevamente las claves");
}
session.setAttribute("cambioClave", cambioClave);
session.removeAttribute("hmAut");
session.removeAttribute("hmSearch");
}
session.setAttribute("opcion", opcion);
HashMap hmAttrs = new HashMap();
hmAttrs.put("givenName", "Alberto");
hmAttrs.put("sn","Velasquez Prado");
hmAttrs.put("mail","avelasquez@nspsac.com");
hmAttrs.put("uid","avelasquez");
hmAttrs.put("cn","Alberto Velasquez Prado");
hmAttrs.put("dni","87654321");
hmAttrs.put("ruc","10876543218");
hmAttrs.put("employeeNumber","6825");
hmAttrs.put("telephoneNumber","051202020");
hmAttrs.put("direccion","Av. El Sol Nro. 560");
hmAttrs.put("st","Puno");
hmAttrs.put("l","Puno");
acceso.addEntry("avelasquez",hmAttrs);
acceso.addAttribute("avelasquez","userPassword","abc");
acceso.modifyAttribute("avelasquez","userPassword","abc1");
//acceso.deleteAttribute("avelasquez","userPassword");
//acceso.deleteEntry("avelasquez");
acceso.renameEntry("pcarrillo","pedro.carrillo");
acceso.renameEntry("jmamani","juan.mamani","Admin");
}
catch(NamingException ne){
ne.printStackTrace();
}
catch(IncompleteConversationalState ics){
ics.printStackTrace();
}
catch(Exception e){
e.printStackTrace();
}
try{
getServletConfig().getServletContext().getRequestDispatcher("/ldap").forward(request, response);
}
catch(Exception e){
e.printStackTrace();
}
}
}
![]() |
| Figura 4: Pantalla de Inicio |
Y por último realizando las búsquedas de entradas de directorios mediante un RDN, autenticando usuarios y cambiando clave de un usuario. Solamente se ha incluido en la página web las funcionalidades mencionadas, las demás opciones ya los puede incluir con los métodos desarrollados en el presente artículo.
<%
HashMap hmAut = (HashMap)session.getAttribute("hmAut");
HashMap hmSearch = (HashMap)session.getAttribute("hmSearch");
String cambioClave = (String)session.getAttribute("cambioClave");
String opcion = (String)session.getAttribute("opcion");
%>
<%//... Para mostrar los datos de una entrada después de autenticar...%>
<%if(hmAut != null){
String uid = (String)hmAut.get("uid");
String sn = (String)hmAut.get("sn");
String givenName = (String)hmAut.get("givenName");
String cn = (String)hmAut.get("cn");
String employeeNumber = (String)hmAut.get("employeeNumber");
String dni = (String)hmAut.get("dni");
String ruc = (String)hmAut.get("ruc");
String direccion = (String)hmAut.get("direccion");
String mail = (String)hmAut.get("mail");
String telephoneNumber = (String)hmAut.get("telephoneNumber");
String mobile = (String)hmAut.get("mobile");
String jpegPhoto = (String)hmAut.get("jpegPhoto");
String msg = (String)hmAut.get("Mensaje");
if(msg != null){%>
<div class="titulo"><%=msg%></div>
<%}%>
<table>
<tr><td><b>Cuenta de Usuario :</b></td> <td><%=uid%></td></tr>
<tr><td><b>Apellidos :</b></td> <td><%=sn%></td></tr>
<tr><td><b>Nombres :</b></td> <td><%=givenName%></td></tr>
<tr><td><b>Nombre Completo :</b></td> <td><%=cn%></td></tr>
<tr><td><b>Registro Empleado :</b></td>
<td><%=(employeeNumber!=null?employeeNumber:"-")%></td></tr>
<tr><td><b>DNI :</b></td> <td><%=dni%></td></tr>
<tr><td><b>RUC :</b></td> <td><%=ruc%></td></tr>
<tr><td><b>Dirección :</b></td>
<td><%=direccion%> - <%=hmAut.get("l")%>,
<%=hmAut.get("st")%></td></tr>
<tr><td><b>Correo Electrónico :</b></td> <td><%=mail%></td></tr>
<tr><td><b>Teléfono :</b></td>
<td><%=(telephoneNumber!=null)?telephoneNumber:"-"%></td></tr>
<tr><td><b>Teléfono Móvil :</b></td> <td><%=(mobile!=null)?mobile:"-"%></td></tr>
<tr>
<td valign="top"><b>Foto :</b></td>
<td>
<%if(jpegPhoto != null) {%>
<img src="imgs/<%=jpegPhoto%>" alt="" border="0" width="70" height="80">
<% } else {%>
-
<% }%>
</td>
</tr>
</table>
<%}%>
<%//... Para mostrar datos de una entrada después de una búsqueda... %>
<%if(hmSearch != null){
String msg = (String)hmSearch.get("Mensaje");
if(msg != null){%>
<div class="titulo"><%=msg%></div>
<%
hmSearch.remove("Mensaje");
}
for (Iterator it = hmSearch.keySet().iterator(); it.hasNext();){
String name = (String)it.next();
String value = (String)hmSearch.get(name);
if(!name.equals("jpegPhoto")){%>
<%=name%>: <%=value%><br>
<%}
else{
%>
<%=name%>: <br><img src="imgs/<%=value%>" alt="" border="0"><br>
<%}
}
}%>
![]() |
| Figura 5: Usuario Autenticado |
Directorios LDAP
Clientes LDAP
JXplorer (Java)
LDAP Browser/Editor (Java)
Luma (Unix)
Frood (Unix)
CoralDirectory LDAP Browser (Windows)
LDAP Exporter (Windows)
maX.500 (Macintosh)
phpLDAPadmin (web)
Librerías para LDAP
Recursos
Referencias y Herramienta utilizadas
[1] OpenLDAP 2.2.29,
http://www.openldap.org/
[2] Netscape Directory SDK 4.1 for Java,
http://www.mozilla.org/
[3] Java Standard Edition 6 Development Kit,
http://java.sun.com/
[4] Apache Jakarta Tomcat 5.5.9,
http://www.apache.org/
[5] LDAP Browser\Editor 2.8.2,
http://www.iit.edu/~gawojar/ldap/
[6] LDAP en Español,
http://www.ldap-es.org/
[7]
Fuente de este artículo
Acerca del autor
Irenio Luis Chagua Aduviri
Irenio trabaja en NetSolutions Perú S.A.C. En sus ratos libres, colabora con javaHispano y desarrolla software.
j2ee 





Reader Comments