viernes, noviembre 27, 2009

Modulo de login personalizado para Tomcat

Continuando con el tema abierto por FiTe en su post "Login de un usuario mediante Realm en un servidor Java EE", vamos a hablar a continuación de cómo desarrollar un módulo de login personalizado, mediante el cual podremos controlar de qué manera nuestros usuarios serán autenticados por nuestra aplicación y cómo estableceremos sus diferentes roles.


Este ejemplo lo he desarrollado usando Netbeans 6.7.1 y Tomcat 6.0.20.


Para empezar, necesitaremos definir las clases UserPrincipal y RolePrincipal, implementando el interfaz java.security.Principal. Este interfaz prové un único método llamado getName().

En primer lugar, la clase UserPrincipal. Añadimos un atributo de la clase String, llamado "name" y un constructor que recibe como parámetro un String, el cual asociamos al atributo de la clase.

package com.aradne.jaas;

import java.security.Principal;

public class UserPrincipal implements Principal {

private String name;

public UserPrincipal(String name) {
this.name = name;
}

public String getName() {
return name;
}

}

En el caso de la clase RolePrincipal, hacemos exáctamente lo mismo:



package com.aradne.jaas;

import java.security.Principal;

public class RolePrincipal implements Principal {

private String name;

public RolePrincipal(String name) {
this.name = name;
}

public String getName() {
return name;
}

}

Lo siguiente que tenemos que hacer es crear nuestro propio LoginModule, implementando el interfaz javax.security.auth.spi.LoginModule. Este interfaz prové los siguientes métodos:

  • initialize
  • login
  • commit
  • abort
  • logout

Este sería el código de la clase:



package com.aradne.jaas;

import java.io.IOException;
import java.util.Map;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;

public class CustomLoginModule implements LoginModule {

private CallbackHandler handler;
private Subject subject;
private String login;

public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
this.handler = callbackHandler;
this.subject = subject;
}

public boolean login() throws LoginException {
Callback[] callbacks = new Callback[2];
callbacks[0] = new NameCallback("login");
callbacks[1] = new PasswordCallback("password", true);
try {
handler.handle(callbacks);
String name = ((NameCallback) callbacks[0]).getName();
String password = String.valueOf(((PasswordCallback) callbacks[1]).getPassword());
//Tenemos el nombre y el password, aquí es donde correpondería validar contra una base de datos, LDAP, AD, etc...
//Para este ejemplo, nos limitamos a comprobar que el nombre y la
//contraseña son "pepe" y "pepe", pero se puede complicar lo que
//queramos.
boolean loginOk = false;
if (name.equals("pepe") && password.equals("pepe")) {
loginOk = true;
}
//Si las credenciales no son correctas devolvemos "false"
if (!loginOk) {
return false;
}
//Si todo ha ido bien, guardamos el nombre del usuario y devolvemos "true"
login = name;
return true;
} catch (IOException e) {
throw new LoginException(e.getMessage());
} catch (UnsupportedCallbackException e) {
throw new LoginException(e.getMessage());
}
}

public boolean commit() throws LoginException {
try {
//Si llegamos a este método es que el usuario se ha autenticado
//de forma correcta.
//Creamos un UserPrincipal con el nombre de usuario y lo almacenamos.
UserPrincipal user = new UserPrincipal(login);
subject.getPrincipals().add(user);

//Creamos un RolePrincipal y lo almacenamos. En este caso, me limito
//a guardar el role "admin", pero en un caso real, tendría que usar
//el nombre del usuario para recuperar una lista de roles. Se pueden
//añadir múltiples roles.
RolePrincipal role = new RolePrincipal("admin");
subject.getPrincipals().add(role);
return true;
} catch (Exception e) {
throw new LoginException(e.getMessage());
}
}

public boolean abort() throws LoginException {
return false;
}

public boolean logout() throws LoginException {
try {
UserPrincipal user = new UserPrincipal(login);
RolePrincipal role = new RolePrincipal("admin");
subject.getPrincipals().remove(user);
subject.getPrincipals().remove(role);
return true;
} catch (Exception e) {
throw new LoginException(e.getMessage());
}
}

}

A continuación, modificamos el archivo META-INF/context.xml de nuestra aplicación web para indicarle a Tomcat el Realm que deseamos usar. Al usar un módulo personalizado, debemos indicar la clase org.apache.catalina.realm.JAASRealm.


El atributo appName es importante, ya que lo usuaremos a posteriori


También debemos indicar las clase que usaremos como UserPrincipal y RolePrincipal.



<Realm className="org.apache.catalina.realm.JAASRealm"
appName="CustomLogin"
userClassNames="com.aradne.jaas.UserPrincipal"
roleClassNames="com.aradne.jaas.RolePrincipal">
</Realm>

Lo siguiente que debemos hacer es configurar la seguridad en el archivo WEB-INF/web.xml. Utilizaremos una autenticación mediante formulario. Algo que FiTe explicó en el artículo que menciono al principio. Al realm le llamo igual que en el context.xml, en mi caso: "CustomLogin"


A continuación, al usar un módulo JAAS personalizado, debemos pasar como parámetro al servidor un archivo de configuración en el que le indicamos cual es el LoginModule a usar para el realm que hemos definido (en nuestro ejemplo, al realm lo hemos llamado "CustomLogin") en el context.xml. Debemos usar este mismo nombre. El archivo de configuración, lo podemos almacenar en cualquier directorio. En mi caso, el archivo es "C:\jaas.config":



CustomLogin {
com.aradne.jaas.CustomLoginModule sufficient debug=true;
};

Ahora vamos a indicarle al servidor cual es el archivo de configuración JAAS. Como para esta demo estoy usando el Tomcat que se incluye con la distribución de Netbeans, voy a la pestaña "Services", expando el nodo "Servers" y edito las propiedades del servidor Tomcat.


Dentro de las propiedades del Tomcat, selecciono la pestaña "Platform" y en la caja de texto "VM Options:", incluyo el siguiente parámetro:

  • -Djava.security.auth.login.config=C:"/jaas.config"

Por supuesto, tendréis que haber creado una página con un formulario de login, etc. Ya podéis probar vuestro propio módulo de login.


Un saludo a todos.

martes, noviembre 17, 2009

Cuando Eclipse deja mostrar errores en los paquetes

Hoy he tenido que invertir una considerable cantidad de tiempo en solucionar un problema.

El problema era que Eclipse, en la vista de paquetes, no me mostraba los paquetes con clases que no compilaban. Lo cual, si lo pensáis, es una gran.... faena.

Después de, cómo no, preguntárselo a Dios y estar enredando por las opciones (por si algo había desconfigurado mi inconsciente), dí con la solución.

El problema había comenzado con un checkout del repositorio de fuentes. Por alguna razón, me había desaparecido de las librerías la referencia a JUnit. De hecho, fijándome con más atención en la vista de Problemas, me aparecía uno advirtiéndome que no podía recompilar el proyecto por que le faltaba una referencia.

Después de añadir JUnit a la lista de librerías, por fin, pude ver de nuevo ese icono redondo, rojo y con un aspa en medio. Nunca antes sentí tanta alegría de verlo. Curioso.

Actualización:
Una divertida variante de este problema es cuando, además de no mostrar errores en los paquetes por faltar alguna librería, es cuando detectas que tras modificar alguna parte del poryecto y lanzar la ejecución del mismo en el servidor (este arranca y despliega la aplicación sin mostrar errores) los cambios no parecen surtir efecto... por más evidentes que los hagas... vuelve a la lista de problemas y tendrás la solución.

martes, octubre 27, 2009

Instalación de OpenCms 7.5.0 en Glassfish 2

Hola,

para los que conozcais la herramienta OpenCms (gestor de contenidos open source), sabréis que se trata de una aplicación web java que se despliega habitualmente sobre un servidor Tomcat.

Sin embargo, por diferentes cuestiones, a veces es necesario desplegarlo sobre otros servidores de aplicaciones y es aquí cuando empezamos a tener problemas. En el caso de Glassfish 2, al que me he tenido que enfrentar, la instalación no funcionaba.

OpenCms se distribuye como un .war, por lo cual su instalación sobre un servidor de aplicaciones no debería dar ningún problema y sin embargo, los da.

En diferentes foros, se nos brindan varias soluciones, ninguna de las cuales nos sirvió, porque se suele hablar de modificar ciertos parámetros en el archivo web.xml o bien, modificar ciertas clases de la herramienta. Seguimos las instrucciones, pero no conseguimos hacerlo funcionar.

Esta pequeña investigación, que nos permitió encontrar una solución, siguió los siguiente pasos:

1.- Instalación "por las bravas"

Dado que la instalación no era posible inicialmente, una de mis compañeras tuvo una idea.

En el mismo servidor donde teníamos instalado el Glassfish, instalamos y arrancamos un Tomcat. Desplegamos la aplicación sobre el Tomcat sin problemas y llevamos a cabo del proceso de instalación. En el caso de OpenCms, la instalación no termina al desplegar el .war, sino que debemos entrar en un wizard que proporciona la aplicación y seguir los pasos necesarios hasta tener el producto operativo.

A continuación, paramos el Tomcat y copiamos la carpeta "opencms", que se encontrará dentro del directorio "webapps" del Tomcat y la llevamos al directorio de despliegue de aplicaciones del Glassfish: "
/domains/domain1/applications/j2ee-modules/".

Tras arrancar el servidor Glassfish, la aplicación OpenCms estaba funcionando perfectamente.

2.- Retocar el war

Una vez teníamos el OpenCms funcionando sin problemas, desarrollamos unos servicios web que debían funcionar sobre la aplicación OpenCms, en el mismo contexto. El caso es no conseguíamos desplegar los servicios web.

La investigación que permitió resolver el problema, también nos dió la clave sobre lo que podía estar impidiendo que el .war se desplegase correctamente en Glassfish.

Como cada servidor de aplicaciones sigue sus propias reglas (esto pasa también con WebSphere, JBoss o Weblogic), aparte de los archivos de configuración estándar, disponen de sus propios archivos adicionales. En el caso de Glassfish, existe un archivo llamado "sun-web.xml" que se debe incluir en el directorio "\WEB-INF" de la aplicación. Para OpenCms, basta con añadir el archivo "sun-web.xml" con el siguiente contenido:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sun-web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Application Server 9.0 Servlet 2.5//EN" "http://www.sun.com/software/appserver/dtds/sun-web-app_2_5-0.dtd">
<sun-web-app error-url="">
<context-root>/opencms</context-root>
<class-loader delegate="true"/>
<jsp-config>
<property name="keepgenerated" value="true">
<description>Keep a copy of the generated servlet class' java code.</description>
</property>
</jsp-config>
</sun-web-app>


La segunda cosa que había que cambiar es la siguiente: en el archivo "web.xml" de Tomcat se refleja una versión anterior del archivo, la 2.4. El encabezado que viene en el archivo dentro del .war que nos bajamos es el siguiente:

<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">


Si lo cambiamos por este otro:


<web-app version="2.5"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">


Podíamos desplegar servicios web sin problemas en la aplicación. En definitiva, la solución para realizar la instalación sin problemas consiste en:

  • Obtener el .war de OpenCms.
  • Editar el .war.
  • Añadir el archivo "sun-web.xml".
  • Modificar la cabecera del archivo "web.xml".
  • Realizar el despliegue con este .war modificado.
Espero que esto os sirva de ayuda a alguno de vosotros.

miércoles, julio 29, 2009

Off-topic: Problemas de acesso a ingdirect.es con Firefox 3.5

Como cliente de ingdirect.es, puede que te hayas encontrado con el problema de no poder usar el acceso de clientes con Firefox (yo uso la versión 3.5).

El problema en concreto que yo he encontrado es que al pulsar el enlace "Acceso clientes", se abre una ventana popup, que se queda en blanco, esperando a cargarse, pero no ocurre nada.

Después de visitar varios foros y ver que hay personas que tienen problemas y otras que no, se me ocurrió que la mayor diferencia que puede haber en Firefox entre diferentes usuarios son los complementos instalados.

Para probar, en el menú Herramientas, seleccionamos la opción Complementos y dentro de complementos, Extensiones. Las extensiones se pueden desactivar (requiere reiniciar Firefox).

Una vez desactivadas las extensiones, se puede acceder a la parte de clientes de ingdirect sin ningún problema, de forma que alguna de ellas está causando un conflicto en esa página concreta.

Yo tengo instaladas las siguientes:

- DownThemAll
- Firebug
- Html Validator
- Java Quick Starter
- Page Speed

No he probado a activarlas una a una para ver cual es la que causa este fallo, pero si quieres usar tu navegador favorito para acceder a tu banco online y estás teniendo este tipo de problemas, al menos de esta forma es posible.

lunes, enero 26, 2009

Login de un usuario mediante Realm en un servidor Java EE

La seguridad en las aplicaciones Web es un tema muy importante a la vez que recurrente. En la mayoría de aplicaciones que he visto o en las que me ha tocado trabajar se ha usado la típica de presentar al usuario un formulario para que introdujese su usuario y contraseña, validarlos contra una base de datos y una vez que se determinaba que las credenciales eran correctas, de una u otra forma, arrastrar esa información por la sesión de la aplicación.

Considero que es una forma muy poco elegante de habilitar la seguridad de una aplicación. Más aún cuando existen otras formas más estándares de validar un usuario en una aplicación. En concreto mediante el uso de Realm (también conocido por Security Police Domain). Este mecanismo forma parte de la especificación JEE v.5, por lo que cualquier servidor de aplicaciones “decente” nos proporcionará los mecanismos necesarios para su implantación y uso.

Dentro de Realm existen a su vez varias alternativas de uso:

  • Digest

  • Client certificate

  • Basic

  • Form



Sin entrar en detalle en cada una de ellas, se puede decir que tanto la Basic como la Digest no son muy aconsejadas.

Este documento se centrará en el tipo Form en un servidor Tomtat 6.0 y usará una base de datos Oracle 9i para validar las credenciales del usuario (existen otras posibilidades, como validar contra un LDAP o un fichero de texto, por ejemplo).

Este tipo de Realm se caracteriza por la presentación al usuario de una página personalizada para el login (a diferencia de la basic/digest, que nos presenta el típico cuadro de diálogo del navegador) cuando este intenta acceder a una URL que consideramos restringida. En este caso, vamos a restringir el acceso a toda la aplicación.

Para habilitar este tipo de seguridad, es necesario la modificación de dos ficheros. Uno es el web.xml (conocido por todos) y otro es el context.xml (bajo el directorio META-INF). De este fichero ya hemos hablado en otra entrada, cuando tratamos la configuración de un pool de conexiones. Es un fichero que se usa para la configuración de recursos de la aplicación de una forma limpia.

En este fichero hemos de crear una entrada parecida a la siguiente:


<realm classname="org.apache.catalina.realm.JDBCRealm"
drivername="oracle.jdbc.OracleDriver"
connectionurl="jdbc:oracle:thin:@nombre_servidor:puerto:SID" digest="MD5"
connectionname="usu_db" connectionpassword="pas_db"
usertable="esquema.nombre_tabla_usuarios" usernamecol="nombre_columna_login"
usercredcol="nombre_columna_password" userroletable="esquema.nombre_tabla_roles"
rolenamecol="nombre_columna_rol">



Veamos un poco más en detalle qué significa cada atributo.

  • className
    Nombre de la clase que el servidor usará para establecer una conexión a la base de datos. Usaremos esta siempre que queramos usar un JDBC en un servidor Tomcat, claro.

  • driverName
    Nombre del driver que se usará para la conexión.

  • connectionURL
    No hay mucho que explicar sobre este punto... simplemente la cadena de conexión usada.

  • digest
    En una aplicación medianamente seria, las contraseñas de los usuarios no se guardarán en formato plano que cualquier “humano” pueda leer, sino que se “pasarán” por algún tipo de algoritmo de encriptación tipo sha o similar. En este caso se ha usado el MD5.

  • connectionName
    Nombre del usuario de la base de datos.

  • connectionPassword
    Contraseña del usuario de la base de datos.


Aquí llega lo interesante. Realm se basa en la validación de usuarios y roles para establecer los permisos a la aplicación (como podremos comprobar en la configuración del fichero web.xml). Por ello, se requiere una tabla de usuarios y otra de roles en las que se buscará al usuario y se recuperarán los roles que tiene asignados.

Siempre que uso Realm utilizo una vista en la que muestro únicamente tres columnas: nombre_usuario, clave y nombre_rol. De este modo queda un poco más “autónomo” de posibles cambios y libre de las restricciones que Realm requiere en las relaciones de las dos tablas.


  • userTable
    Nombre de la tabla que contiene los registros de los usuarios (en mi caso, nombre de la vista que tiene los datos del login, contraseña y rol del usuario).

  • userNameCol
    Nombre de la columna donde se encuentra el “login” del usuario.

  • userCredCol
    Nombre de la columna donde se encuentra la contraseña del usuario.

  • userRoleTable
    Nombre de la tabla que contiene los registros de los roles (en mi caso, nombre de la vista que tiene los datos del login, contraseña y rol del usuario).

  • roleNameCol
    Nombre de la columna donde se encuentra el nombre del rol.


Bien, con esto habríamos terminado con el fichero config.xml. Sólo una nota. Podríamos haber utilizado una conexión directa (sin usar JDBC) a la base de datos o también podríamos usar un “data source” ya existente (el elemento Realm cambia, en los dos casos, en sus atributos). En el caso de usar un data source existente este debería estar definido en el server.xml del servidor Tomcat o en el web.xml de la aplicación. Ya que si hacemos referencia a uno declarado en el mismo fichero config.xml no funcionaría.

Ahora necesitamos realizar unos cambios en el fichero web.xml. Deberemos añadir algo parecido a esto (asegurate de que respetas el orden de los elementos en el fichero web.xml):



<security-constraint>

<display-name>Constraint1</display-name>

<web-resource-collection>

<web-resource-name>NombreRecurso</web-resource-name>

<description>

<url-pattern>/*</url-pattern>

<http-method>GET</http-method>

<http-method>POST</http-method>

</description>

<auth-constraint>

<description>

<role-name>nombre_rol1</role-name>

<role-name>nombre_rol2</role-name>

</description>

</auth-constraint>

<login-config>

<auth-method>FORM</auth-method>

<realm-name>

<form-login-config>

<form-login-page>/login.html</form-login-page>

<form-error-page>/login_error.html</form-error-page>

</form-login-config>

</realm-name>

<security-role>

<description>

<role-name>nombre_rol1</role-name>

</description>

<security-role>

<description>

<role-name>nombre_rol2</role-name>

</description>



Para explicarlo, podemos dividirla en tres grupos. El primero sería el grupo de “security-constrain”. Bien, en este establece establece a través de patrones qué recursos de nuestra aplicación estarán restringidos (elementos url-pattern. Puede haber más de uno. En este caso es toda la aplicación, pero podría usarse, por ejemplo “pages/*.jsp u otras... atención a la barra inicial), bajo qué tipo de petición http (elementos http-method) y qué roles serán los que tengan acceso a esos recursos (elementos role-name).

El segundo grupo define que tipo de autenticación estamos usando (elemento auth-method) y, muy importante, cuál será la página que se presentará para la introducción de las credenciales del usuario en caso de que se detecte un acceso restringido (elemento form-login-page) y cuál la que se presentará en caso de producirse un error en la validación de las credenciales (elemento form-error-page).

El tercer grupo define los roles de los usuarios activos para la aplicación (elementos role-name). Estos deben ser los mismos que aparezcan en el campo nombre_rol de la tabla nombre_tabla_rol que hemos establecido en el fichero config.xml.

Bien, con todo esto configurado, sólo nos queda reiniciar nuestro Tomcat y acceder a la raíz de nuestra aplicación. Si todo está correcto, nos deberá aparecer la página de login que hemos establecido en la configuración.

Un detalle a tener en cuenta es que, al tener toda nuestra aplicación con acceso restringido, también los recursos (estilos, imágenes...) que use nuestra página de login (e incluso la propia página) también estarán restringidos, por lo que quizás tengas que darle un par de vueltas al diseño para conseguir que “luzca” como de costumbre.

Una vez valides correctamente a un usuario a través de su login y contraseña podrás recuperar el login introducido a través del método "getRemoteUser()" del objeto HttpServletRequest y recuperar el resto de sus datos o tratarlo como creas conveniente. Lo que es seguro es que nadie llegará a una página que consideres con acceso restringido sin pasar antes por el login.

Ahora, sólo os queda comenzar a probar con distintas configuraciones para establecer distintos niveles de acceso a los usuarios. Que lo disfruteis.