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.

3 comentarios:

Juniorpop dijo...

Muy buen articulo, lo voy a probar.

Silfide dijo...

Buenas, yo estoy poco ducho en estas cosas, sabes qué cambios se tendrían que hacer para recoger nombres de usuarios, passwords y roles de un archivo?
Gracias

FiTe dijo...

Hola Silfide...

No entiendo muy bien cómo puedes tener el control de accesos a una aplicación web en un fichero, a no ser que sea para "jugar", pero para gustos.... :)

En verdad el único cambio que deberías hacer es implementar el acceso al/os fichero/s en cuestión en el método "login" de la clase CustonLoginModule justo después de recuperar la contraseña y el usuario de los callbacks.

De todos modos, si vas a usar un fichero y usas un Tomcat, quizás sería mejor que usases el Realm UserDatabaseRealm de la implementación que este servidor trae y que es más "lógica" que hacer una propia desde un fichero.

Esta implementación es la que viene configurada para acceder al manager del servidor y se accede a través del fichero tomcar-users.xml que puedes encontrar en $TOMCAT_HOME/conf de tu instalación. Desconozco si otros servidores tienen esta "facilidad".

Échale un vistazo la docu de Reaml de tomcat. Viene perfectamente explicado.

Un saludo...