Introducción
Para implementar el cifrado y descifrado de campos específicos en MyBatis-Plus, existen dos enfoques principales:
- Método basado en TypeHandler personalizado de MyBatis-Plus
- 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.