前言:因公司需求需要把某些实体类的某些字段值进行加密保存,在查询时解密明文输出。现记录两种方式。
一、第一种方式:
(1)使用@TableField(typeHandler = TypeHandler.class)注解自带的字段类型处理器,写一个 handle 类继承 BaseTypeHandler。本身这个是用来处理字段类型转换的,如map转list之类的,这里也可以用作值加密。
注:使用这种方式有个前提,需要在@TableName注解上加上autoResultMap = true
,才能使查询生效,否则只是新增修改生效进行拦截。没看源码猜想应该是先查整个实体有没有autoResultMap = true
,再逐个进行字段拦截处理优化效率。
@Data
@TableName(value = "xxx",autoResultMap = true)
public class xxx implements Serializable {private static final long serialVersionUID = 1L;@TableId(type = IdType.NONE, value = "id")private String id;@ApiModelProperty(value = "用户名")@TableField(value = "username",typeHandler = TypeControlHandler.class)private String username;@ApiModelProperty(value = "手机号")@TableField(value = "phone",typeHandler = TypeControlHandler.class)private String phone;
}
(2)定义加解密方式-自定义(这里配合使用了mysql的AES_ENCRYPT
和AES_DECRYPT
函数加解密,主要是为了实现模糊查询,具体加解密方式可以自定义)
//需要导包:
//<dependency>
// <groupId>commons-codec</groupId>
// <artifactId>commons-codec</artifactId>
//</dependency>import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;@Slf4j
public class DBAesUtils {
/** AES 加解密密钥,请勿擅自修改!!! */public static final String key = "唯一加解密密匙key-自定义";/*** AES 加密 使用AES-128-ECB加密模式* @param sSrc 需要加密的字段* @param sKey 16 位密钥* @return* @throws Exception*/public static String Encrypt(String sSrc, String sKey) {try {if (sKey == null) {return null;}/** 判断Key是否为16位 */if (sKey.length() != 16) {return null;}byte[] raw = sKey.getBytes("utf-8");SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");/** "算法/模式/补码方式" */Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");cipher.init(Cipher.ENCRYPT_MODE, skeySpec);byte[] encrypted = cipher.doFinal(sSrc.getBytes("utf-8"));/** 此处使用BASE64做转码功能,同时能起到2次加密的作用。 */return new Base64().encodeToString(encrypted);} catch (Exception e) {e.printStackTrace();return null;}}public static String Encrypt(String sSrc) {return Encrypt(sSrc, key);}/*** AES 解密 使用AES-128-ECB加密模式* @param sSrc 需要解密的字段* @param sKey 16 位密钥* @return* @throws Exception*/public static String Decrypt(String sSrc, String sKey) {try {// 判断Key是否正确if (sKey == null) {return null;}// 判断Key是否为16位if (sKey.length() != 16) {return null;}byte[] raw = sKey.getBytes("utf-8");SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");cipher.init(Cipher.DECRYPT_MODE, skeySpec);/** 先用base64解密 */byte[] encrypted1 = new Base64().decode(sSrc);try {byte[] original = cipher.doFinal(encrypted1);String originalString = new String(original,"utf-8");return originalString;} catch (Exception e) {System.out.println(e.toString());return null;}} catch (Exception e) {e.printStackTrace();return null;}}public static String Decrypt(String sSrc) {return Decrypt(sSrc, key);}}
(3)定义具体拦截处理实现
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;public class TypeControlHandler extends BaseTypeHandler<String> {@Overridepublic void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {ps.setString(i, DBAesUtils.Encrypt(parameter));}@Overridepublic String getNullableResult(ResultSet rs, String columnName) throws SQLException {return DBAesUtils.Decrypt(rs.getString(columnName));}@Overridepublic String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {return DBAesUtils.Decrypt(rs.getString(columnIndex));}@Overridepublic String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {return DBAesUtils.Decrypt(cs.getString(columnIndex));}}
(4)最终实现结果(有个小问题就是如果加密前字符一样,加密出来密文就是一样的)
至于加密的字段查询并没有想到好的方法,目前想到的只能不使用LambdaQueryWrapper
,使用QueryWrapper
写死条件进行查询,这里配合了mysql的加解密函数,其他数据库需更改加解密方式才能使用。
wrapper.like("AES_DECRYPT(FROM_BASE64(字段名),'" + key + "')", 查询值);
第二种方式(其实实现原理就是第一种方式,只不过加了相对来说更灵活的扩展):
(1)定义两个注解
/*** 需要加解密的字段用这个注解* @author wangshaopeng@talkweb.com.cn* @Date 2023-05-31*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface EncryptedColumn {}/*** 需要加解密的实体类用这个注解* @author wangshaopeng@talkweb.com.cn* @Date 2023-05-31*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface EncryptedTable {}
(2)实现InnerInterceptor
进行拦截加密操作(使用第一种方法的DBAesUtils
工具类进行加密)
import cn.hutool.core.util.ReflectUtil;
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
import com.baomidou.mybatisplus.core.conditions.update.Update;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.springframework.core.annotation.AnnotationUtils;import java.lang.reflect.Field;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;@SuppressWarnings({"rawtypes"})
public class EncryptInterceptor extends JsqlParserSupport implements InnerInterceptor {/*** 变量占位符正则*/private static final Pattern PARAM_PAIRS_RE = Pattern.compile("#\\{ew\\.paramNameValuePairs\\.(" + Constants.WRAPPER_PARAM + "\\d+)\\}");@Overridepublic void beforeUpdate(Executor executor, MappedStatement mappedStatement, Object parameterObject) throws SQLException {if (Objects.isNull(parameterObject)) {return;}// 通过MybatisPlus自带API(save、insert等)新增数据库时if (!(parameterObject instanceof Map)) {if (needToDecrypt(parameterObject.getClass())) {encryptEntity(parameterObject);}return;}Map paramMap = (Map) parameterObject;Object param;// 通过MybatisPlus自带API(update、updateById等)修改数据库时if (paramMap.containsKey(Constants.ENTITY) && null != (param = paramMap.get(Constants.ENTITY))) {if (needToDecrypt(param.getClass())) {encryptEntity(param);}return;}// 通过在mapper.xml中自定义API修改数据库时if (paramMap.containsKey("entity") && null != (param = paramMap.get("entity"))) {if (needToDecrypt(param.getClass())) {encryptEntity(param);}return;}// 通过UpdateWrapper、LambdaUpdateWrapper修改数据库时if (paramMap.containsKey(Constants.WRAPPER) && null != (param = paramMap.get(Constants.WRAPPER))) {if (param instanceof Update && param instanceof AbstractWrapper) {Class<?> entityClass = mappedStatement.getParameterMap().getType();if (needToDecrypt(entityClass)) {encryptWrapper(entityClass, param);}}return;}}/*** 校验该实例的类是否被@EncryptedTable所注解*/private boolean needToDecrypt(Class<?> objectClass) {try {EncryptedTable sensitiveData = AnnotationUtils.findAnnotation(objectClass, EncryptedTable.class);return Objects.nonNull(sensitiveData);}catch (Exception ex){return false;}}/*** 通过API(save、updateById等)修改数据库时** @param parameter*/private void encryptEntity(Object parameter) {//取出parameterType的类Class<?> resultClass = parameter.getClass();Field[] declaredFields = ReflectUtil.getFields(resultClass);for (Field field : declaredFields) {//取出所有被EncryptedColumn注解的字段EncryptedColumn sensitiveField = field.getAnnotation(EncryptedColumn.class);if (!Objects.isNull(sensitiveField)) {field.setAccessible(true);Object object = null;try {object = field.get(parameter);} catch (IllegalAccessException e) {continue;}//只支持String的解密if (object instanceof String) {String value = (String) object;//对注解的字段进行逐一加密try {field.set(parameter, DBAesUtils.Encrypt(value));} catch (IllegalAccessException e) {continue;}}}}}/*** 通过UpdateWrapper、LambdaUpdateWrapper修改数据库时** @param entityClass* @param ewParam*/private void encryptWrapper(Class<?> entityClass, Object ewParam) {AbstractWrapper updateWrapper = (AbstractWrapper) ewParam;String sqlSet = updateWrapper.getSqlSet();String[] elArr = sqlSet.split(",");Map<String, String> propMap = new HashMap<>(elArr.length);Arrays.stream(elArr).forEach(el -> {String[] elPart = el.split("=");propMap.put(elPart[0], elPart[1]);});//取出parameterType的类Field[] declaredFields = ReflectUtil.getFields(entityClass);for (Field field : declaredFields) {//取出所有被EncryptedColumn注解的字段EncryptedColumn sensitiveField = field.getAnnotation(EncryptedColumn.class);if (Objects.isNull(sensitiveField)) {continue;}String el = propMap.get(field.getName());Matcher matcher = PARAM_PAIRS_RE.matcher(el);if (matcher.matches()) {String valueKey = matcher.group(1);Object value = updateWrapper.getParamNameValuePairs().get(valueKey);updateWrapper.getParamNameValuePairs().put(valueKey, DBAesUtils.Encrypt(value.toString()));}}}}
(3)实现Interceptor
进行拦截解密
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.core.annotation.AnnotationUtils;
import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Objects;@Intercepts({@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class DecryptInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {Object resultObject = invocation.proceed();if (Objects.isNull(resultObject)) {return null;}if(resultObject instanceof IPage){IPage page = (IPage) resultObject;if(page!=null&& CollUtil.isNotEmpty(page.getRecords())){if (needToDecrypt(page.getRecords().get(0))) {for (Object result : page.getRecords()) {//逐一解密decrypt(result);}}}}else if (resultObject instanceof ArrayList) {//基于selectListArrayList resultList = (ArrayList) resultObject;if (CollUtil.isNotEmpty(resultList) && needToDecrypt(resultList.get(0))) {for (Object result : resultList) {//逐一解密decrypt(result);}}} else if (needToDecrypt(resultObject)) {//基于selectOnedecrypt(resultObject);}return resultObject;}/*** 校验该实例的类是否被@EncryptedTable所注解*/private boolean needToDecrypt(Object object) {Class<?> objectClass = object.getClass();EncryptedTable sensitiveData = AnnotationUtils.findAnnotation(objectClass, EncryptedTable.class);return Objects.nonNull(sensitiveData);}@Overridepublic Object plugin(Object o) {if(o instanceof ResultSetHandler) {return Plugin.wrap(o, this);}else{return o;}}private <T> T decrypt(T result) throws Exception {//取出resultType的类Class<?> resultClass = result.getClass();Field[] declaredFields = ReflectUtil.getFields(resultClass);for (Field field : declaredFields) {//取出所有被EncryptedColumn注解的字段EncryptedColumn sensitiveField = field.getAnnotation(EncryptedColumn.class);if (!Objects.isNull(sensitiveField)) {field.setAccessible(true);Object object = field.get(result);//只支持String的解密if (object instanceof String) {String value = (String) object;//对注解的字段进行逐一解密String results = DBAesUtils.Decrypt(value);if(StrUtil.isNotEmpty(results)){field.set(result, results);}else{field.set(result, value);}}}}return result;}
}
注:若是作为单独的starter进行封装,此拦截器需要定义在spring.factories
中
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.xxx.xxx.xxx.xxx.DecryptInterceptor
(4)实体类加上注解(需要@EncryptedTable
和@EncryptedColumn
配合使用)
@Data
@TableName(value = "xxx",autoResultMap = true)
@EncryptedTable
public class xxx implements Serializable {private static final long serialVersionUID = 1L;@TableId(type = IdType.NONE, value = "id")private String id;@ApiModelProperty(value = "用户名")@TableField(value = "username")@EncryptedColumnprivate String username;@ApiModelProperty(value = "手机号")@TableField(value = "phone")@EncryptedColumnprivate String phone;
}
实现结果与第一种方式一样