Creando un diálogo de Login con SwingX, Spring y JPA: Persistencia y capa DAO


En la entrada anterior describimos brevemente un escenario que resulta muy común y que en principio parece sencillo pero tiene mucho trabajo de fondo: un diálogo de Login para autenticación de usuarios. En esta entrada veremos cómo implementar el acceso de usuarios comenzando por la base de datos, para luego ir subiendo en las capas o niveles de nuestro ejemplo.

Nota aclaratoria: la elección de usar una Base de datos para este ejemplo es si se quiere arbitraria, dado que hoy en día existen otros mecanismos de autenticación de usuario. Por ejemplo, al tratarse de una aplicación de escritorio podríamos querer utilizar un servicio LDAP, o incluso podríamos querer autenticar a los usuarios utilizando algún Web service, al cual le enviamos el par usuario-password y nos devuelve un token si la autenticación fue exitosa. En cualquier caso, el uso de base de datos con usuarios sigue siendo bastante común y veremos más adelante que gracias a la separación en capas de la aplicación esto no resulta un problema en lo absoluto: nuestro diseño será altamente reutilizable.

El origen: la Base de datos

A continuación se encuentra el script de creación de la base de datos que utilizaremos en nuestro ejemplo (servidor MySQL 5.6)  pero antes quisiera destacar dos aspectos que tienen que ver con seguridad a nivel de base de datos:

  • Uso de Vistas para encapsular los datos de inicio de sesión: es muy común ver una base de datos en la cual los datos de inicio de sesión y los datos personales de un usuario se encuentran en la misma tabla, digamos “Usuarios”. Idealmente los datos de inicio de sesión deberían estar una tabla separada y vincularse de manera unívoca a través de un ID con los datos personales del usuario, lo cual generalmente no ocurre en bases de datos legacy. Pero podemos simular esta separación haciendo uso de una Vista que sólo nos provea los datos de inicio de sesión de la tabla “Usuarios” a los efectos de autenticar un usuario.
  • Uso de un Rol o Usuario específico a nivel de base de datos para las operaciones de autenticación: este aspecto es fundamental y va de la mano con el punto anterior. Por motivos de seguridad, el rol o usuario de base de datos con el cual se va a conectar nuestra aplicación para efectuar la autenticación debe tener la menor cantidad de privilegios posibles. En otras palabras, ese Rol o Usuario debe poder operar con las tablas u objetos asociados al proceso de autenticación y nada más.

Quizás ambas medidas parezcan extremas o incluso rayan la paranoia, pero consideremos lo siguiente: el Login o inicio de sesión es la puerta de entrada de nuestra aplicación y estamos exponiendo la base de datos desde una puerta común a todo aquel que la utilice. Si por algún motivo un usuario lograra inyectar código SQL malicioso – tipo de ataque muy común conocido como Inyección SQL– a través del Login y el mismo código llegara a ejecutarse en la Base de datos, la misma debería estar preparada para contener esa situación y anular el ataque. Con las dos medias anteriores logramos lo siguiente:

  • Si el SQL malicioso llega a la base de datos, el Rol o Usuario no tendrá permisos para ejecutarlo, quedando sin efecto el ataque.
  • Si el SQL malicioso hace un SELECT  de todos los usuarios en la Vista que creamos, sólo obtendrá usuarios y contraseñas -las cuales deberían ser en realidad los hashes y no las contraseñas en texto plano- pero no datos privados de los usuarios, reduciendo el impacto del ataque.

En un mundo ideal este tipo de consideraciones los corresponden a un equipo de Seguridad y DBA pero no siempre es el caso y de todos modos es bueno que sepamos que hay que tener estos recaudos al desarrollar aplicaciones.

Finalmente, un último detalle netamente de implementación es el uso de un valor por defecto para el timestamp de inicio de sesión y un procedimiento almacenado para finalizar una sesión. Esta decisión obedece en el hecho que la aplicación será de escritorio y por lo tanto delegaremos la tarea de mantener consistencia con respecto a datos temporales al servidor de base de datos.

--
-- Script de Creación de la base de datos.
--
DROP DATABASE IF EXISTS EjemploLogin;
CREATE DATABASE IF NOT EXISTS EjemploLogin;
USE EjemploLogin;

--
-- Tabla Usuario
--
DROP TABLE IF EXISTS Usuario;
CREATE TABLE Usuario (
    /*
     * Datos de inicio de sesión
     */
    id_usuario SERIAL PRIMARY KEY,
    nombre_usuario VARCHAR(30) NOT NULL,
    password VARCHAR(200),
    bloqueado BOOLEAN DEFAULT FALSE,
    /*
     * Datos de dominio
     */
    nombre VARCHAR(200),
    apellido VARCHAR(200),
    fecha_de_nacimiento DATE,
    UNIQUE(nombre_usuario)
);
INSERT INTO Usuario(nombre_usuario, password, nombre, apellido) 
    VALUES('john.doe', 'DeepCafe', 'John', 'Doe');

--
-- Vista Usuario_Login
--
DROP VIEW IF EXISTS Usuario_Login;
CREATE VIEW Usuario_Login AS 
    SELECT id_usuario, nombre_usuario, password, bloqueado
      FROM Usuario;    

--
-- Tabla Sesion
--
DROP TABLE IF EXISTS Sesion;
CREATE TABLE Sesion (
    id_sesion SERIAL PRIMARY KEY,
    id_usuario BIGINT UNSIGNED NOT NULL,
    inicio_sesion DATETIME DEFAULT NOW(),
    fin_sesion DATETIME DEFAULT NULL,
    FOREIGN KEY (id_usuario) REFERENCES Usuario (id_usuario)
);

--
-- Creamos un stored procedure para finalizar una sesión.
--
DROP PROCEDURE IF EXISTS finalizarSesion;

DELIMITER //

CREATE PROCEDURE finalizarSesion(IN id_sesion_in BIGINT UNSIGNED)
BEGIN
    UPDATE EjemploLogin.Sesion SET fin_sesion = NOW() 
        WHERE id_sesion = id_sesion_in;
END
//

DELIMITER ;

-- 
-- Creamos un usuario 'login'@'%' y le asignamos permisos 
-- para operar con la vista Usuario_Login y la tabla Sesion
--
DROP USER 'login'@'%';
CREATE USER 'login'@'%' IDENTIFIED BY 'LoginApp';
GRANT USAGE ON EjemploLogin.* TO 'login'@'%';

GRANT SELECT ON EjemploLogin.Usuario_Login TO 'login'@'%';
GRANT SELECT, INSERT, UPDATE ON EjemploLogin.Sesion TO 'login'@'%';
GRANT EXECUTE ON PROCEDURE EjemploLogin.finalizarSesion TO 'login'@'%';

--
-- Fin Script de creación
-- 

Mapeo Objeto-Relacional con JPA

Una vez que creamos la base de datos, procedemos a crear nuestra unidad de persistencia –persistence.xml–  las entidades JPA que van a mapear la tabla Sesion y la vista Usuario_Login de la base de datos. Por las dudas, vale la aclaración: para JPA una Vista se mapea exactamente igual que una tabla, por lo cual no tendremos mayores complicaciones al respecto.

UsuarioLogin.java

Esta entidad es completamente de sólo lectura, dado que no crearemos nuevos usuarios si no simplemente consultaremos la base de datos por los usuarios ya existentes. Lo mencionado anteriormente, el hecho que Usuario_Login sea una vista no afecta en nada el mecanismo de mapeo.

package com.javadeepcafe.entities;

import java.io.Serializable;
import java.math.BigInteger;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

/**
 * @author Delcio Amarillo
 */
@Entity
@Table(name = "Usuario_Login")
public class UsuarioLogin implements Serializable {
    
    private static final long serialVersionUID = 2796360555765047008L;
    
    @Id 
    @Column(name = "id_usuario")
    private BigInteger idUsuario;
    
    @Column(name = "nombre_usuario")
    private String nombreUsuario;
    
    @Column(name = "password")
    private String password;
    
    @Column(name = "bloqueado")
    private Boolean bloqueado;
    
    public UsuarioLogin() {}

    public BigInteger getId() {
        return idUsuario;
    }

    public String getNombreUsuario() {
        return nombreUsuario;
    }

    public String getPassword() {
        return password;
    }

    public Boolean getBloqueado() {
        return bloqueado;
    }
    
    /*
     * Overriden from Object
     */

    @Override
    public int hashCode() {
        return idUsuario != null ? idUsuario.hashCode() : 0;
    }

    @Override
    public boolean equals(Object object) {
        if (!(object instanceof UsuarioLogin)) {
            return false;
        }
        UsuarioLogin other = (UsuarioLogin) object;
        return !((this.idUsuario == null && other.idUsuario != null) 
                    || (this.idUsuario != null && !this.idUsuario.equals(other.idUsuario)));
    }

    @Override
    public String toString() {
        return String.format("%1$1s[%2$1s]", getClass().getName(), idUsuario);
    }
}

Sesion.java

En este caso sí crearemos nuevos objetos Sesion en la base de datos y por lo tanto debemos definir la estrategia de generación del ID de la entidad y el mapeo con la vista Usuario_Login. Es necesario notar que aquí la clave foránea que define la relación apunta a la Vista Usuario_Login y no a la tabla Usuarios. Esto es correcto dado que la vista es precisamente una visión acotada de la tabla Usuarios y por lo tanto los identificadores o ID’s serán los mismos en cada fila de ambas. Desde el punto de vista de la base de datos, la integridad requerida se mantiene.

package com.javadeepcafe.entities;

import java.io.Serializable;
import java.math.BigInteger;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

/**
 * @author Delcio Amarillo
 */
@Entity
@Table(name = "Sesion")
public class Sesion implements Serializable {
    
    private static final long serialVersionUID = -1221718121500357998L;
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id_sesion")
    private BigInteger idSesion;
    
    @Temporal(value = TemporalType.TIMESTAMP)
    @Column(name = "inicio_sesion", insertable = false)
    private Date inicioSesion;
    
    @Temporal(value = TemporalType.TIMESTAMP)
    @Column(name = "fin_sesion", insertable = false)
    private Date finSesion;
    
    @ManyToOne
    @JoinColumn(name = "id_usuario", referencedColumnName = "id_usuario")
    private UsuarioLogin usuario;
    
    public Sesion() {}

    public BigInteger getId() {
        return idSesion;
    }

    public Date getInicioSesion() {
        return inicioSesion != null ? new Date(inicioSesion.getTime()) : null;
    }
    
    public Date getFinSesion() {
        return finSesion != null ? new Date(finSesion.getTime()) : null;
    }
    
    public UsuarioLogin getUsuario() {
        return usuario;
    }

    public void setUsuario(UsuarioLogin usuario) {
        this.usuario = usuario;
    }
    
    /*
     * Overriden from Object
     */

    @Override
    public int hashCode() {
        return idSesion != null ? idSesion.hashCode() : 0;
    }

    @Override
    public boolean equals(Object object) {
        if (!(object instanceof Sesion)) {
            return false;
        }
        Sesion other = (Sesion) object;
        return !((this.idSesion == null && other.idSesion != null) 
            || (this.idSesion != null && !this.idSesion.equals(other.idSesion)));
    }

    @Override
    public String toString() {
        return String.format("%1$1s[%2$1s]", getClass().getName(), idSesion);
    }
}

persistence.xml

A continuación el archivo persistence.xml que define nuestra unidad de persistencia. Cabe aclarar que la elección de Eclipselink se debe a que es la implementación de referencia de JPA. Si bien Spring tiene su propia implementación y ofrece mecanismos propios para trabajar con JPA en lo personal prefiero utilizar el estándar por sobre implementaciones particulares de los frameworks.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" 
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
    <persistence-unit name="DemoLoginPU" transaction-type="RESOURCE_LOCAL">
        <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
        <class>com.javadeepcafe.entities.UsuarioLogin</class>
        <class>com.javadeepcafe.entities.Sesion</class>
        <properties>
            <property name="javax.persistence.jdbc.url" 
                value="jdbc:mysql://ubuntu-server:3306/EjemploLogin?zeroDateTimeBehavior=convertToNull"/>
            <property name="javax.persistence.jdbc.user" value="login"/>
            <property name="javax.persistence.jdbc.password" value="LoginApp"/>
            <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
            <!-- Propiedades del Logger de EclipseLink -->
            <property name="eclipselink.logging.logger" value="DefaultLogger"/>
            <property name="eclipselink.logging.level.sql" value="FINE"/>
            <property name="eclipselink.logging.parameters" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

Capa DAO con JPA y Spring

Llegó el momento de implementar la capa DAO de nuestra aplicación, para lo cual utilizaremos JPA estándar para queries y Spring para inyección de dependencias. Algo que me gustaría aclarar es que las siguientes clases deberían implementar alguna interfaz. Es decir, los métodos de estas clases deberían estar definidos en interfaces para que la aplicación sea más flexible. He omitido las interfaces ex profeso para evitar que las entradas sean aun más extensas, pero confío en que los lectores tomarán los recaudos que corresponden.

UsuarioLoginRepository.java

package com.javadeepcafe.repositories;

import com.javadeepcafe.entities.UsuarioLogin;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import org.springframework.stereotype.Repository;

/**
 * @author Delcio Amarillo
 */
@Repository
public class UsuarioLoginRepository {
    
    private static final Logger LOGGER = 
        Logger.getLogger(UsuarioLoginRepository.class.getName());
    
    @PersistenceContext(unitName = "DemoLoginPU")
    private EntityManager entityManager;
    
    public UsuarioLogin getUsuario(String userName) {
        UsuarioLogin usuario = null;
        try {
            String jpql = "SELECT u FROM UsuarioLogin u WHERE u.nombreUsuario = :userName";
            TypedQuery<UsuarioLogin> query = entityManager.createQuery(jpql, UsuarioLogin.class);
            query.setParameter("userName", userName);
            usuario = query.getSingleResult();
        } catch (NoResultException ex) {
            LOGGER.log(Level.FINE, userName, ex);
        }
        return usuario;
    }
}

SesionRepository.java

En esta clase podemos ver dos particularidades que no son muy frecuentes.

  • Una es el uso de flush() y refresh(…) dentro del método crearSesion(…) los cuales son llamados porque el timestamp de inicio de sesión se genera en la base de datos y de otro modo nuestro objeto tendrá null como valor de inicio de sesión. En general JPA sólo consulta por los ID’s que son autogenerados pero automáticamente no hace un refresh del objeto para evitar hacer SELECT más a la base de datos luego del INSERT. A decir verdad el caso que haya valores generados del lado de la base de datos es poco frecuente y si por defecto se hiciera un refresh(…) se estaría penalizando con una llamada a la base de datos todas las operaciones de persistencia, lo cual no es deseable.
  • La otra es la llamada a un procedimiento almacenado en el método finalizarSesion(…). A modo de opinión personal, esta interfaz de JPA es sencillamente genial y nos permite llamar a procedimientos almacenados de un modo unificado sin importar si el motor de base de datos es Oracle, MySQL, PosgreSQL, MS SQLServer, etcétera.
package com.javadeepcafe.repositories;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.StoredProcedureQuery;
import org.springframework.stereotype.Repository;
import com.javadeepcafe.entities.Sesion;
import com.javadeepcafe.entities.UsuarioLogin;
import java.math.BigInteger;
import javax.persistence.ParameterMode;

/**
 * @author Delcio Amarillo
 */
@Repository
public class SesionRepository {
    
    @PersistenceContext(unitName = "DemoLoginPU")
    private EntityManager entityManager;
    
    public Sesion crearSesion(UsuarioLogin usuario) {
        if (usuario == null) {
            throw new IllegalArgumentException("El parámetro [usuario] no puede ser null!");
        }
        Sesion sesion = new Sesion();
        sesion.setUsuario(usuario);
        entityManager.persist(sesion);
        entityManager.flush();
        entityManager.refresh(sesion);
        return sesion;
    }
    
    public void finalizarSesion(Sesion sesion) {
        if (sesion == null) {
            throw new IllegalArgumentException("El parámetro [sesion] no puede ser null");
        }
        StoredProcedureQuery query = entityManager.createStoredProcedureQuery("finalizarSesion");
        query.registerStoredProcedureParameter("sesion_id", BigInteger.class, ParameterMode.IN);
        query.setParameter("sesion_id", sesion.getId());
        query.execute();
    }
}

Configuración de Spring para soportar persistencia

Antes de concluir con esta entrada, creo oportuno ver cómo configuramos Spring para que soporte JPA y nos permita inyectar nuestro EntityManager en los repositories. Para quienes no hayan utilizado Spring este framework admite dos formas de configuración: usando XML o usando anotaciones. En este ejemplo utilizaremos anotaciones.

BeansConfig.java

Esta clase es la que vamos a utilizar para decirle a Spring cómo debe instanciar los diferentes Beans que queremos inyectar a lo largo del ciclo de vida de nuestra aplicación. Aquí no veremos la clase completa, sólo la parte correspondiente a la capa de persistencia.  La anotación @EnableTransactionManagement y la creación del bean PlatformTransactionManager tienen que ver con el manejo de transacciones, el cual veremos mejor en la capa de Servicios en la siguiente entrada.

package com.javadeepcafe.config;

...
import com.javadeepcafe.repositories.SesionRepository;
import com.javadeepcafe.repositories.UsuarioLoginRepository;
...
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
 * @author Delcio Amarillo
 */
@Configuration
@EnableTransactionManagement
public class BeansConfig {
    
    @Bean
    public LocalEntityManagerFactoryBean createEntityManagerFactory() {
        LocalEntityManagerFactoryBean factory = new LocalEntityManagerFactoryBean();
        factory.setPersistenceUnitName("DemoLoginPU");
        return factory;
    }
    
    @Bean
    public PlatformTransactionManager createPlatformTransactionManager() {
        return new JpaTransactionManager(createEntityManagerFactory().getObject());
    }
    
    @Bean
    public SesionRepository createSesionRepository() {
        return new SesionRepository();
    }
    
    @Bean
    public UsuarioLoginRepository createUsuarioLoginRepository() {
        return new UsuarioLoginRepository();
    }
    ...
}

Continuará…

En esta entrada vimos aspectos de seguridad a nivel de la base de datos, cómo mapear las tablas/vistas utilizando JPA y cómo implementar nuestra capa DAO usando JPA y Spring. En la siguiente entrada veremos cómo implementar nuestra capa de Servicio en la cual comenzaremos a ver el back-end del componente JXLoginPane de SwingX.

 

 

 

Anuncios

2 pensamientos en “Creando un diálogo de Login con SwingX, Spring y JPA: Persistencia y capa DAO

  1. Wilmar Giraldo

    Debe haber un error en esta línea : UPDATE EjemploLogin.Sesion SET NOW() en el script de Stored Procedure, supongo que debe ser: UPDATE EjemploLogin.Sesion SET fin_sesion = NOW()

    Saludos.

    Me gusta

    Responder

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