Implementación de cifrado y descifrado de campos específicos con MyBatis-Plus

Introducción

Para implementar el cifrado y descifrado de campos específicos en MyBatis-Plus, existen dos enfoques principales:

  1. Método basado en TypeHandler personalizado de MyBatis-Plus
  2. Método basado en interceptores de MyBatis

En este artículo utilizaremos el segundo enfoque. Se trata de una solución transparente que permite cifrar datos sensibles antes de almacenarlos en la base de datos y descifarlos automáticamente al recuperarlos.

Definición de anotaciones personalizadas

Primero, definimos dos anotaciones que nos permitirán marcar las clases y campos que requieren tratamiento de cifrado.

import java.lang.annotation.*;

/**
 * Anotación para indicar que la clase contiene campos cifrados
 * Se aplica a nivel de clase de entidad
 */
@Documented
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DatoCifrado {
}

import java.lang.annotation.*;

/**
 * Anotación para marcar campos individuales que requieren cifrado/descifrado
 * Se aplica a nivel de campo
 */
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CampoCifrado {
}

Utilidades criptográficas

CipherCore

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;

public class CipherCore {

    public CipherCore() {
    }

    public static byte[] procesar(byte[] contenido, Key clave, byte[] vector, int modo, String algoritmo) {
        byte[] resultado = null;
        try {
            Cipher cifrador;
            if (vector == null) {
                cifrador = Cipher.getInstance(algoritmo + "/ECB/PKCS5Padding");
            } else {
                cifrador = Cipher.getInstance(algoritmo + "/CBC/PKCS5Padding");
                IvParameterSpec especificacionIv = new IvParameterSpec(vector);
                cifrador.init(modo, clave, especificacionIv);
            }
            resultado = cifrador.doFinal(contenido);
        } catch (Exception excepcion) {
            excepcion.printStackTrace();
        }
        return resultado;
    }
}

AesUtils

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class AesUtils {
    private static final Base64.Encoder codificador = Base64.getEncoder();
    private static final Base64.Decodificador decodificador = Base64.getDecoder();

    public AesUtils() {
    }

    public static String cifrar(String contenido, String claveSecreta) {
        byte[] resultado = cifrar(contenido.getBytes(StandardCharsets.UTF_8), 
                                  claveSecreta.getBytes(StandardCharsets.UTF_8));
        return codificador.encodeToString(resultado);
    }

    public static byte[] cifrar(byte[] contenido, byte[] claveBytes) {
        return procesar(contenido, claveBytes, null, Cipher.ENCRYPT_MODE);
    }

    public static String cifrar(String contenido, String claveSecreta, String vector) {
        byte[] resultado = cifrar(contenido.getBytes(StandardCharsets.UTF_8), 
                                  claveSecreta.getBytes(StandardCharsets.UTF_8), 
                                  vector.getBytes(StandardCharsets.UTF_8));
        return codificador.encodeToString(resultado);
    }

    public static byte[] cifrar(byte[] contenido, byte[] claveBytes, byte[] vectorIv) {
        return procesar(contenido, claveBytes, vectorIv, Cipher.ENCRYPT_MODE);
    }

    public static String descifrar(String contenido, String claveSecreta) {
        byte[] resultado = descifrar(decodificador.decode(contenido), 
                                     claveSecreta.getBytes(StandardCharsets.UTF_8));
        return new String(resultado, StandardCharsets.UTF_8);
    }

    public static byte[] descifrar(byte[] contenido, byte[] claveBytes) {
        return procesar(contenido, claveBytes, null, Cipher.DECRYPT_MODE);
    }

    public static String descifrar(String contenido, String claveSecreta, String vector) {
        byte[] resultado = descifrar(decodificador.decode(contenido), 
                                     claveSecreta.getBytes(StandardCharsets.UTF_8), 
                                     vector.getBytes(StandardCharsets.UTF_8));
        return new String(resultado, StandardCharsets.UTF_8);
    }

    public static byte[] descifrar(byte[] contenido, byte[] claveBytes, byte[] vectorIv) {
        return procesar(contenido, claveBytes, vectorIv, Cipher.DECRYPT_MODE);
    }

    private static byte[] procesar(byte[] contenido, byte[] claveBytes, byte[] vectorIv, int modo) {
        SecretKeySpec especificacionClave = new SecretKeySpec(claveBytes, "AES");
        return CipherCore.procesar(contenido, especificacionClave, vectorIv, modo, "AES");
    }
}

Implementación de interceptores

Interceptor de cifrado de parámetros

import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.Map;
import java.util.Properties;

@Component
@Intercepts({
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class CifradoParametrosInterceptor implements Interceptor {

    private static final String CLAVE_SECRETA = "MnViZXJsYWRvckNvbXB1dG8xMjM=";
    private static final String VECTOR_INICIAL = "QUJDREVGR0hJSktM";

    @Override
    public Object intercept(Invocation invocacion) throws Throwable {
        Object objeto = invocacion.getArgs()[1];
        MappedStatement declaracionMapeada = (MappedStatement) invocacion.getArgs()[0];
        SqlCommandType tipoComandoSql = declaracionMapeada.getSqlCommandType();

        if (SqlCommandType.INSERT.equals(tipoComandoSql) || "UPDATE".equals(tipoComandoSql.name())) {
            procesarObjeto(objeto);
        }

        Object resultado = invocacion.proceed();
        
        if (objeto instanceof MapperMethod.ParamMap) {
            Map mapa = (Map) objeto;
            Object entidad = mapa.containsKey("param1") ? mapa.get("param1") : mapa.get("et");
            restaurarCamposCifrados(entidad);
        } else {
            restaurarCamposCifrados(invocacion.getArgs()[1]);
        }
        
        return resultado;
    }

    private void procesarObjeto(Object objeto) throws IllegalAccessException {
        if (objeto == null) return;
        
        Class> clase = objeto.getClass();
        boolean tieneAnotacion = clase.isAnnotationPresent(DatoCifrado.class);
        
        if (tieneAnotacion) {
            cifrarCampos(objeto, clase.getDeclaredFields());
        } else if (objeto instanceof MapperMethod.ParamMap) {
            Map mapa = (Map) objeto;
            Object entidad = mapa.containsKey("param1") ? mapa.get("param1") : mapa.get("et");
            if (entidad != null) {
                Clase> tipoEntidad = entidad.getClass();
                if (tipoEntidad.isAnnotationPresent(DatoCifrado.class)) {
                    cifrarCampos(entidad, tipoEntidad.getDeclaredFields());
                }
            }
        }
    }

    private void cifrarCampos(Object objeto, Field[] campos) throws IllegalAccessException {
        for (Field campo : campos) {
            if (campo.isAnnotationPresent(CampoCifrado.class)) {
                campo.setAccessible(true);
                Object valor = campo.get(objeto);
                if (valor != null) {
                    String contenidoCifrado = cifrar(String.valueOf(valor));
                    campo.set(objeto, contenidoCifrado);
                }
            }
        }
    }

    private void restaurarCamposCifrados(Object objeto) {
        // No es necesario restaurar aquí, se maneja en el interceptor de resultados
    }

    @Override
    public Object plugin(Object objetivo) {
        return Plugin.wrap(objetivo, this);
    }

    @Override
    public void setProperties(Properties propiedades) {
    }

    private String cifrar(String contenido) {
        return AesUtils.cifrar(contenido, CLAVE_SECRETA, VECTOR_INICIAL);
    }
}

Interceptor de descifrado de resultados

import org.apache.ibatis.executor.resultset.DefaultResultSetHandler;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.List;
import java.util.Objects;
import java.util.Properties;

@Component
@Intercepts({
    @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = Statement.class)
})
public class DescifradoResultadosInterceptor implements Interceptor {

    private static final String CLAVE_SECRETA = "MnViZXJsYWRvckNvbXB1dG8xMjM=";
    private static final String VECTOR_INICIAL = "QUJDREVGR0hJSktM";

    @Override
    public Object intercept(Invocation invocacion) throws Throwable {
        DefaultResultSetHandler manejadorResultados = (DefaultResultSetHandler) invocacion.getTarget();
        MetaObject metaObjeto = SystemMetaObject.forObject(manejadorResultados);
        
        MappedStatement declaracionMapeada = (MappedStatement) metaObjeto.getValue("mappedStatement");
        List<resultmap> mapasResultado = declaracionMapeada.getResultMaps();
        Clase> tipoResultado = mapasResultado.get(0).getType();
        
        boolean requiereDescifrado = tipoResultado.isAnnotationPresent(DatoCifrado.class);
        Object resultadoObjeto = invocacion.proceed();
        
        if (requiereDescifrado && !Objects.isNull(resultadoObjeto)) {
            List lista = (List) resultadoObjeto;
            for (Object elemento : lista) {
                descifrarCampos(elemento);
            }
        }
        
        return resultadoObjeto;
    }

    private void descifrarCampos(Object elemento) throws IllegalAccessException {
        Field[] campos = elemento.getClass().getDeclaredFields();
        for (Field campo : campos) {
            if (campo.isAnnotationPresent(CampoCifrado.class)) {
                campo.setAccessible(true);
                Object valor = campo.get(elemento);
                if (valor != null) {
                    String contenidoDescifrado = descifrar(String.valueOf(valor));
                    campo.set(elemento, contenidoDescifrado);
                }
            }
        }
    }

    @Override
    public Object plugin(Object objetivo) {
        return Plugin.wrap(objetivo, this);
    }

    @Override
    public void setProperties(Properties propiedades) {
    }

    private String descifrar(String contenidoCifrado) {
        try {
            byte[] datosDescifrados = AesUtils.descifrar(contenidoCifrado, CLAVE_SECRETA, VECTOR_INICIAL);
            return new String(datosDescifrados, "UTF-8");
        } catch (Exception excepcion) {
            excepcion.printStackTrace();
            return contenidoCifrado;
        }
    }
}
</resultmap>

Aplicación en entidades

Para utilizar el sistema de cifrado, basta con aplicar las anotaciones a la clase de entidad y a los campos correspondientes:

@Data
@Entity
@TableName("usuarios")
@DatoCifrado
public class Usuario {
    
    @TableId(type = IdType.AUTO)
    private Long id;
    
    private String nombre;
    
    private String correoElectronico;
    
    @CampoCifrado
    private String numeroTelefono;
    
    @CampoCifrado
    private String numeroDocumento;
    
    private LocalDateTime fechaCreacion;
    
    private LocalDateTime fechaActualizacion;
}

Funcionamiento

Una vez configurados los interceptores y aplicadas las anotaciones, el funcionamiento es completamente transparente:

  • Las operaciones INSERT y UPDATE cifrarán automáticamente los campos marcados antes de guardar en la base de datos.
  • Las operaciones SELECT descifrarán automáticamente los campos marcados al recuperar los resultados.
  • Los datos almacenados en la base de datos permanecerán cifrados, protegiendo la información sensible.

Esta implementación proporciona una capa de seguridad adicional para datos confidenciales sin requerir cambios en la lógica de negocio de la aplicación.

Etiquetas: MyBatis-Plus java encrypt decrypt interceptor

Publicado el 6-8 21:12