Creando un diálogo de Login con SwingX, Spring y JPA: Capa de Servicio


En la entrada anterior definimos la base de datos, vimos el mapeo objeto-relaiconal asociado y la capa de persistencia o DAO que utilizaremos en nuestro ejemplo.  Ha llegado el turno de ver la capa de Servicio de nuestra aplicación, algunos detalles que tienen que ver con manejo de transacciones con Spring y cómo integrar esta capa con el componente JXLoginPane.

Antes de comenzar, recordemos brevemente que en la primera entrega de la serie vimos las siguientes clases e interfaces que actúan como back-end del framework de autenticación que provee el componente JXLoginPane:

  • LoginService: clase abstracta que debe implementar la lógica de autenticación de las credenciales ingresadas en el componente JXLoginPane. Internamente mantiene una lista con objetos que implementan la interfaz LoginListener, los cuales son notificados mediante LoginEvents durante todo el proceso: login iniciado, cancelado, exitoso o fallido.
  • PasswordStore: clase abstracta cuya finalidad es la de almacenar las contraseñas ingresadas por los usuarios en una especie de caché, con el objetivo de ayudar en el proceso de login. El verdadero mecanismo y la forma de persistencia de las contraseñas se deja a cargo de las implementaciones concretas. De este modo podemos guardar contraseñas en un archivo plano local, una base de datos auxiliar, un archivo properties o xml, etcétera.
  • UserNameStore: igual que PasswordStore pero para nombres de usuario.

En lo que respecta al framework de autenticación, en nuestro ejemplo implementaremos el servicio de  login y el servicio de almacén de nombre de usuarios. El servicio de almacén de contraseñas no está contemplado pero no es muy diferente al de nombre de usuarios, razón por la cual queda como ejercicio para quien quiera completarlo.

Servicio de Login

LoginServiceImp.java

Recordemos que en la primera entrada mencionamos que el 90% de nuestro trabajo consiste en implementar un sólo método de la clase abstracta LoginService. Pues bien, gracias a JPA y Spring, esto será realmente muy sencillo.

package com.javadeepcafe.services;

import org.jdesktop.swingx.auth.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import com.javadeepcafe.entities.UsuarioLogin;
import com.javadeepcafe.repositories.UsuarioLoginRepository;
import org.springframework.stereotype.Service;

/**
 * @author Delcio Amarillo
 */
@Service
public class LoginServiceImp extends LoginService {
    
    @Autowired
    private UsuarioLoginRepository usuarioLoginRepository;

    @Override
    public boolean authenticate(String userName, char[] password, String server) throws Exception {
        UsuarioLogin usuario = usuarioLoginRepository.getUsuario(userName);
        return usuario != null 
            && String.valueOf(password).equals(usuario.getPassword()) 
            && !usuario.getBloqueado();
    }
}

El mecanismo implementado aquí es muy sencillo: le pedimos al Repository un usuario con el nombre ingresado en el díalogo de login. Si este usuario existe y su contraseña coincide con la ingresada y además no se encuentra bloqueado, entonces retornamos true. En caso contrario retornamos false.

¿Qué pasa si guardamos los hashes de las contraseñas, o las guardamos cifradas?

Pues nada, sólo debemos obtener el hash de la contraseña que ingresó el usuario en el diálogo de login y compararlo con el que está guardado en la base de datos. Lo mismo si la contraseña se guarda cifrada. En ambos casos, obviamente, la función de hash/cifrado a aplicar debe ser la misma que se utilizó al momento de guardar la contraseña del usuario.

¿Qué pasa si queremos controlar el número de veces que un usuario intenta autenticarse errónamente para detectar un posible ataque y bloquear el usuario?

Dentro del método authenticate(…) deberíamos efectuar el control, por ejemplo guardando en una tabla temporal los intentos fallidos por usuario y bloqueando el mismo si se excede cierto número de intentos, digamos 5 intentos, antes que el método retorne true o false.

Otro mecanismo sería adjuntar un LoginListener que implemente el método loginFailed(…)  al LoginService y que efectúe dicho control. En mi opinión este mecanismo sería mejor en lo que respecta a división de responsabilidades: el servicio autentica y el listener resuelve qué hacer con los eventos de login. Sin embargo existe un inconveniente y es que los métodos de la interfaz LoginListener admiten como parámetro un evento LoginEvent el cual no nos provee información de los datos de login (usuario, password, servidor) que generaron el evento y por lo tanto desde el listener no podemos saber quién intentó autenticarse errónamente.

Servicio de almacén de usuarios

UserNameStoreService.java

Esta clase no es obligatoria y en realidad representa un extra destinado a mejorar la experiencia de usuario. El servicio de almacén de datos nos permite proveer un caché de nombres de usuario que se traducirá en un autocompletado en el campo Usuario del diálogo de login. Parece algo trivial pero lo cierto es que hoy en día la función de autocompletado es muy común incluso en formularios web y para un usuario recurrente de nuestra aplicación podría resultar tedioso estar ingresando su nombre cada vez que quiere acceder a la misma.

Ahora bien, lo primero que podríamos pensar es en consultar la base de datos a medida que el usuario va ingresando caracteres en el campo Usuario. Esto si bien es correcto, no es muy eficiente desde el punto de vista de performance: hacer una consulta a la base de datos por cada caracter que el usuario ingresa es demasiado costoso. Debemos pensar entonces en un mecanismo que vaya guardando los usuarios localmente, ya sea un archivo o algún otro mecanismo más eficiente. Es por este motivo que usaremos Preferences API de Java, la cual fue diseñada para guardar personalizaciones y configuraciones de nuestra aplicación de un modo totalmente transparente e independiente de la plataforma en la cual se ejecuta nuestra aplicación.

package com.javadeepcafe.services;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.prefs.Preferences;
import org.jdesktop.swingx.auth.UserNameStore;
import org.springframework.stereotype.Service;

/**
 * @author Delcio Amarillo
 */
@Service
public class UserNameStoreService extends UserNameStore {
    
    private List<String> userNames = new ArrayList<>();

    @Override
    public String[] getUserNames() {
        loadUserNames();
        return userNames.toArray(new String[userNames.size()]);
    }

    @Override
    public void setUserNames(String[] names) {
        userNames = Arrays.asList(names);
    }

    @Override
    public void loadUserNames() {
        Preferences prefs = Preferences.userRoot().node(getClass().getName());
        String users = prefs.get("JavaDeepCafeUsers", "");
        userNames = users.isEmpty() ? new ArrayList<String>() : Arrays.asList(users.split(";"));
    }

    @Override
    public void saveUserNames() {
        StringBuilder sb = new StringBuilder();
        for (String user : userNames) {
            sb.append(user).append(";");
        }
        Preferences prefs = Preferences.userRoot().node(getClass().getName());
        prefs.put("JavaDeepCafeUsers", sb.toString());
    }

    @Override
    public boolean containsUserName(String name) {
        return userNames.contains(name);
    }

    @Override
    public void addUserName(String userName) {
        if (!userNames.contains(userName)) {
            userNames.add(userName);
            saveUserNames();
        }
    }

    @Override
    public void removeUserName(String userName) {
        userNames.remove(userName);
        saveUserNames();
    }
}

¿Cómo y dónde se guardan los nombres de usuario con nuestra implementación?

Los nombres de usuario se guardan como un String donde cada nombre está separado por punto y coma. Ese String se guarda de forma diferente dependiendo de la plataforma en la que se ejecuta nuestra aplicación. Por ejemplo en Windows se guardan en el registro del sistema operativo, lo cual tiene sentido porque es el mecanismo histórico en el que las aplicaciones han guardado información de configuración. Podemos encontrar los valores en el siguiente nodo:

Equipo\HKEY_CURRENT_USER\SOFTWARE\JavaSoft\Prefs\com.javadeepcafe.services./User/Name/Store/Service

ejemplo_login_registro

¿Cómo invocamos los métodos del servicio de almacén de usuarios?

Esto es lo mejor de todo: no debemos hacerlo. Simplemente implementando la clase abstracta UserNameStore y adjuntadola al LoginService es suficiente. Las llamadas son transaparentes para nosotros y se reflejan en el diálogo de login, como veremos en la siguiente entrada.

Servicio de sesión

SesionService.java

Para completar el flujo de autenticación e inicio de sesión nos falta un servicio de sesión que cree una nueva sesión si la autenticación fue satisfactoria y mantenga los datos de la sesión activa para poder utilizarlos a lo largo de toda la ejecución de nuestra aplicación.

En principio este servicio debería ser provisto por una clase que implemente el patrón Singleton, de modo que a lo largo de la aplicación la referencia a la sesión activa sea siempre la misma. De esto nos abstrae Spring, cuyo scope por defecto para la instanciación de beans es Singleton y por lo tanto al llamar al método getSesionActiva() estaremos seguros que obtendremos el mismo objeto en cada llamada.

Además me gustaría hacer mención en este punto al manejo de transacciones. Veremos que en el método crearNuevaSesión(…) hay una llamada a dos beans de la capa DAO: UsuarioLoginRepository y SesionRepository. Desde el punto de vista del modelo de negocios, ambas llamadas deben ocurrir dentro de una misma transacción atómica. Esto es, tiene sentido que la obtención del usuario asociado a la sesión y la creación de la sesión misma tengan lugar dentro de una misma transacción. Del mismo modo la llamada al método finalizarSesión(…) debe ser transaccional para asegurar la atomicidad de la operación.

Algo muy natural cuando comenzamos a ver el manejo de transacciones es asociar las mismas a la capa DAO, pensando en bases de datos. Sin embargo el mismo concepto se puede trasladar a la capa de Servicio y pensar en transacciones que tengan que ver con el modelo de negocio asociado a nuestra aplicación, más allá de la implementación de la persistencia. Esto nos permite asociar llamadas a distintas clases DAO dentro de una misma transacción, como veremos en la implementación del servicio de sesión.

package com.javadeepcafe.services;

import com.javadeepcafe.entities.Sesion;
import com.javadeepcafe.entities.UsuarioLogin;
import com.javadeepcafe.repositories.SesionRepository;
import com.javadeepcafe.repositories.UsuarioLoginRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author Delcio Amarillo
 */
@Service
public class SesionService {
    
    private Sesion sesionActiva;
    
    @Autowired
    private SesionRepository sesionRepository;
    
    @Autowired
    private UsuarioLoginRepository usuarioLoginRepository;
    
    @Transactional
    public void crearNuevaSesion(String userName) {
        if (userName == null || userName.isEmpty()) {
             throw new IllegalArgumentException("El parámetro [userName] no puede ser null ni vacío!");
        }
        UsuarioLogin usuario = usuarioLoginRepository.getUsuario(userName);
        sesionActiva = sesionRepository.crearSesion(usuario);
    }
    
    @Transactional
    public void finalizarSesionActiva() {
        if (sesionActiva != null) {
            sesionRepository.finalizarSesion(sesionActiva);
        }
    }

    public Sesion getSesionActiva() {
        return sesionActiva;
    }
}

Configuración de Spring para soportar nuestra capa de Servicios

A continuación veamos la configuración de Spring asociada a nuestra capa de servicios.

BeansConfig.java

package com.javadeepcafe.config;

...
import com.javadeepcafe.services.LoginServiceImp;
import com.javadeepcafe.services.SesionService;
import com.javadeepcafe.services.UserNameStoreService;
import org.jdesktop.swingx.auth.LoginService;
import org.jdesktop.swingx.auth.UserNameStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
...
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
 * @author Delcio Amarillo
 */
@Configuration
@EnableTransactionManagement
public class BeansConfig {
    
    ...
    
    @Bean
    public LoginService createLoginService() {
        return new LoginServiceImp();
    }
    
    @Bean
    public SesionService createSesionService() {
        return new SesionService();
    }
    
    @Bean
    public UserNameStore createUserNameStore() {
        return new UserNameStoreService();
    }
    ...
}

Continuará…

En esta entrada vimos la capa de Servicio de nuestra aplicación, algunos detalles que tienen que ver con manejo de transacciones con Spring y cómo se integra esta capa con el componente JXLoginPane. En la siguiente entrada veremos la capa de Vista de nuestra aplicación, en especial el uso del componente JXLoginPane y cómo podemos reutilizar toda la arquitectura que hemos visto hasta el momento.

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s