Mostrando entradas con la etiqueta seguridad. Mostrar todas las entradas
Mostrando entradas con la etiqueta seguridad. Mostrar todas las entradas

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.

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.