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.