Mybatis拦截器对数据库敏感字段加解密
一、前言
公司业务需求,需要对客户敏感字段进行加密处理,其实挺头疼的,因为有很多数据要处理,第一版我们做的做法,就是在dao层上写一层代理类,把所有的dao层的接口全部实现一遍处理加解密,service引入写的代理类,这样处理其实很麻烦,代码维护方面以及可读性都很差,于是百度搜了很多方式想通过一些方式能不能统一处理,网上看了很久很多没有符合我的,要么就是写了一半,只能看他的方向,搜了很多篇文章,找到一个还不错,自己拿来完善用了起来.
二、思想
mybatis拦截Executor.class对象中的query,update方法,拦截将参数重新处理,这里对实体类,单个参数,以及多个参数处理,map(未实现)通过注解的方式动态对字段的值进行加解密,将查询的结果进行解密拦截返回.
三、本项目的Springboot项目,具体实现如下
1.使用的加密是Aes加密(加密方式多多,自己选择)
import com.*.bean.enums.AesEnum;
import org.thymeleaf.util.StringUtils;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* @Description 修改工具类
* 新增前端交互加密解密AES工具类(RandomStringUtils 随机生成key和偏移量)
*/
public class AESUtil {
private static final String AES_KEY = "jafndsafa";
private static final String AES_IV = "411231411313";
public static String encrypt(String value, AesEnum aesEnum) {
try {
return assemble(value, aesEnum.getKey(), aesEnum.getIv());
} catch (Exception e) {
return null;
}
}
public static String encrypt(String value) {
try {
return assemble(value, AES_KEY, AES_IV);
} catch (Exception e) {
return null;
}
}
public static String decrypt(String secValue, AesEnum aesEnum) {
if (StringUtils.isEmpty(secValue)) return null;
try {
return disassemble(secValue, aesEnum.getKey(), aesEnum.getIv());
} catch (Exception e) {
return null;
}
}
public static String decrypt(String secValue) {
if (StringUtils.isEmpty(secValue)) return null;
try {
return disassemble(secValue, AES_KEY, AES_IV);
} catch (Exception e) {
return null;
}
}
public static String encrypt(String value, String key, String iv) {
try {
return assemble(value, key, iv);
} catch (Exception e) {
return null;
}
}
public static String decrypt(String secValue, String key, String iv) {
try {
return disassemble(secValue, key, iv);
} catch (Exception e) {
return null;
}
}
private static String assemble(String sSrc, String sKey, String ivStr) throws Exception {
if (sKey == null) {
return null;
}
if (sKey.length() != 16) {
return null;
}
byte[] raw = sKey.getBytes();
SecretKeySpec sKeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
if (ivStr != null) {
IvParameterSpec iv = new IvParameterSpec(ivStr.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, sKeySpec, iv);
} else {
cipher.init(Cipher.ENCRYPT_MODE, sKeySpec);
}
byte[] encrypted = cipher.doFinal(sSrc.getBytes());
return byte2hex(encrypted).toLowerCase();
}
private static String disassemble(String sSrc, String sKey, String ivStr) throws Exception {
try {
if (sKey == null) {
return null;
}
if (sKey.length() != 16) {
return null;
}
byte[] raw = sKey.getBytes("ASCII");
SecretKeySpec sKeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
if (ivStr != null) {
IvParameterSpec iv = new IvParameterSpec(ivStr.getBytes());
cipher.init(Cipher.DECRYPT_MODE, sKeySpec, iv);
} else {
cipher.init(Cipher.DECRYPT_MODE, sKeySpec);
}
byte[] encrypted1 = hex2byte(sSrc);
try {
byte[] original = cipher.doFinal(encrypted1);
String originalString = new String(original);
return originalString;
} catch (Exception e) {
return null;
}
} catch (Exception ex) {
return null;
}
}
public static byte[] hex2byte(String strhex) {
if (strhex == null) {
return null;
}
int l = strhex.length();
if (l % 2 == 1) {
return null;
}
byte[] b = new byte[l / 2];
for (int i = 0; i != l / 2; i++) {
b[i] = (byte) Integer.parseInt(strhex.substring(i * 2, i * 2 + 2), 16);
}
return b;
}
public static String byte2hex(byte[] b) {
String hs = "";
String stmp = "";
for (int n = 0; n < b.length; n++) {
stmp = (Integer.toHexString(b[n] & 0XFF));
if (stmp.length() == 1) {
hs = hs + "0" + stmp;
} else {
hs = hs + stmp;
}
}
return hs.toUpperCase();
}
}
2.自定义注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @decription DecryptField
* <p>字段解密注解</p>
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptField {
String value() default "";
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @decription EncryptField
* <p>字段加密注解</p>
*/
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptField {
String value() default "";
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.FIELD,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptMethod {
String[] encrypt() default "";
String[] decrypt() default "";
}
3.自定义拦截代码如下:
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import com.*.annotation.EncryptField;
import com.*.annotation.EncryptMethod;
import com.*.util.CryptPojoUtils;
import com.*.util.JsonUtil;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class DataInterceptor implements Interceptor {
private final Logger logger = LoggerFactory.getLogger(DataInterceptor.class);
static int MAPPED_STATEMENT_INDEX = 0;
static int PARAMETER_INDEX = 1;
static int ROWBOUNDS_INDEX = 2;
static int RESULT_HANDLER_INDEX = 3;
static String ENCRYPTFIELD = "1";
static String DECRYPTFIELD = "2";
private static boolean ENCRYPT_SWTICH = true;
/**
* 是否进行加密查询
*
* @return 1 true 代表加密 0 false 不加密
*/
private boolean getFuncSwitch() {
return ENCRYPT_SWTICH;
}
/**
* 校验执行器方法 是否在白名单中
*
* @param statementid
* @return true 包含 false 不包含
*/
private boolean isWhiteList(String statementid) {
boolean result = false;
// String whiteStatementid = "com.*.dao.UserDao.save";
String whiteStatementid = "";
if (whiteStatementid.indexOf(statementid) != -1) {
result = true;
}
return result;
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
logger.info("EncryptDaoInterceptor.intercept开始执行==> ");
MappedStatement statement = (MappedStatement) invocation.getArgs()[MAPPED_STATEMENT_INDEX];
Object parameter = invocation.getArgs()[PARAMETER_INDEX];
logger.info(statement.getId() + "未加密参数串:" + JsonUtil.DEFAULT.toJson(parameter));
/*
* 判断是否拦截白名单 或 加密开关是否配置,
* 如果不在白名单中,并且本地加密开关 已打开 执行参数加密
*/
if (!isWhiteList(statement.getId()) && getFuncSwitch()) {
parameter = encryptParam(parameter, invocation);
logger.info(statement.getId() + "加密后参数:" + JsonUtil.DEFAULT.toJson(parameter));
}
invocation.getArgs()[PARAMETER_INDEX] = parameter;
Object returnValue = invocation.proceed();
logger.info(statement.getId() + "未解密结果集:" + JsonUtil.DEFAULT.toJson(returnValue));
returnValue = decryptReslut(returnValue, invocation);
logger.info(statement.getId() + "解密后结果集:" + JsonUtil.DEFAULT.toJson(returnValue));
logger.info("EncryptDaoInterceptor.intercept执行结束==> ");
return returnValue;
}
/**
* 解密结果集
*
* @param @param returnValue
* @param @param invocation
* @param @return
* @return Object
* @throws
*/
public Object decryptReslut(Object returnValue, Invocation invocation) {
MappedStatement statement = (MappedStatement) invocation.getArgs()[MAPPED_STATEMENT_INDEX];
if (returnValue != null) {
if (returnValue instanceof ArrayList<?>) {
List<?> list = (ArrayList<?>) returnValue;
List<Object> newList = new ArrayList<Object>();
if (1 <= list.size()) {
for (Object object : list) {
Object obj = CryptPojoUtils.decrypt(object);
newList.add(obj);
}
returnValue = newList;
}
} else if (returnValue instanceof Map) {
String[] fields = getEncryFieldList(statement, DECRYPTFIELD);
if (fields != null) {
returnValue = CryptPojoUtils.getDecryptMapValue(returnValue, fields);
}
} else {
returnValue = CryptPojoUtils.decrypt(returnValue);
}
}
return returnValue;
}
/***
* 针对不同的参数类型进行加密
* @param @param parameter
* @param @param invocation
* @param @return
* @return Object
* @throws
*
*/
public Object encryptParam(Object parameter, Invocation invocation) {
MappedStatement statement = (MappedStatement) invocation.getArgs()[MAPPED_STATEMENT_INDEX];
try {
if (parameter instanceof String) {
if (isEncryptStr(statement)) {
parameter = CryptPojoUtils.encryptStr(parameter);
}
} else if (parameter instanceof Map) {
String[] fields = getEncryFieldList(statement, ENCRYPTFIELD);
if (fields != null) {
parameter = CryptPojoUtils.getEncryptMapValue(parameter, fields);
}
} else {
parameter = CryptPojoUtils.encrypt(parameter);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
logger.info("EncryptDaoInterceptor.encryptParam方法异常==> " + e.getMessage());
}
return parameter;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
/**
* 获取参数map中需要加密字段
*
* @param statement
* @param type
* @return List<String>
* @throws
*/
private String[] getEncryFieldList(MappedStatement statement, String type) {
String[] strArry = null;
Method method = getDaoTargetMethod(statement);
Annotation annotation = method.getAnnotation(EncryptMethod.class);
if (annotation != null) {
if (type.equals(ENCRYPTFIELD)) {
strArry = ((EncryptMethod) annotation).encrypt();
} else if (type.equals(DECRYPTFIELD)) {
strArry = ((EncryptMethod) annotation).decrypt();
} else {
strArry = null;
}
}
return strArry;
}
/**
* 获取Dao层接口方法
*
* @param @return
* @return Method
* @throws
*/
private Method getDaoTargetMethod(MappedStatement mappedStatement) {
Method method = null;
try {
String namespace = mappedStatement.getId();
String className = namespace.substring(0, namespace.lastIndexOf("."));
String methedName = namespace.substring(namespace.lastIndexOf(".") + 1, namespace.length());
Method[] ms = Class.forName(className).getMethods();
for (Method m : ms) {
if (m.getName().equals(methedName)) {
method = m;
break;
}
}
} catch (SecurityException e) {
e.printStackTrace();
logger.info("EncryptDaoInterceptor.getDaoTargetMethod方法异常==> " + e.getMessage());
return method;
} catch (ClassNotFoundException e) {
e.printStackTrace();
logger.info("EncryptDaoInterceptor.getDaoTargetMethod方法异常==> " + e.getMessage());
return method;
}
return method;
}
/**
* 判断字符串是否需要加密
*
* @param @param mappedStatement
* @param @return
* @return boolean
* @throws
*/
private boolean isEncryptStr(MappedStatement mappedStatement) throws ClassNotFoundException {
boolean reslut = false;
try {
Method m = getDaoTargetMethod(mappedStatement);
m.setAccessible(true);
Annotation[][] parameterAnnotations = m.getParameterAnnotations();
if (parameterAnnotations != null && parameterAnnotations.length > 0) {
for (Annotation[] parameterAnnotation : parameterAnnotations) {
for (Annotation annotation : parameterAnnotation) {
if (annotation instanceof EncryptField) {
reslut = true;
}
}
}
}
} catch (SecurityException e) {
e.printStackTrace();
logger.info("EncryptDaoInterceptor.isEncryptStr异常:==> " + e.getMessage());
reslut = false;
}
return reslut;
}
}
类中使用工具如下:
import com.*.annotation.DecryptField;
import com.*.annotation.EncryptField;
import org.apache.commons.lang3.StringUtils;
import java.lang.reflect.Field;
import java.util.Map;
/**
* @decription CryptPojoUtils
* <p>对象加解密工具
* 通过反射,对参数对象中包含指定注解的字段进行加解密。
* 调用<tt>encrypt(T t)</tt>方法实现加密,返回加密后的对象;
* 调用<tt>decrypt(T t)</tt>实现解密,返回解密后的对象;
* <tt>encrypt</tt>对注解{@link EncryptField}字段有效;
* <tt>decrypt</tt>对注解{@link DecryptField}字段有效。</p>
*/
public class CryptPojoUtils {
/**
* 对象t注解字段加密
*
* @param t
* @param <T>
* @return
*/
public static <T> T encrypt(T t) {
if (isEncryptAndDecrypt(t)) {
Field[] declaredFields = t.getClass().getDeclaredFields();
try {
if (declaredFields != null && declaredFields.length > 0) {
for (Field field : declaredFields) {
if (field.isAnnotationPresent(EncryptField.class) && field.getType().toString().endsWith("String")) {
field.setAccessible(true);
String fieldValue = (String) field.get(t);
if (StringUtils.isNotEmpty(fieldValue)) {
field.set(t, AESUtil.encrypt(fieldValue));
}
field.setAccessible(false);
}
}
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
return t;
}
/**
* 对含注解字段加密
* @param t
* @param <T>
*/
public static <T> void encryptField(T t) {
Map<String,Object> map = (Map<String, Object>) t;
Field[] declaredFields = t.getClass().getDeclaredFields();
try {
if (declaredFields != null && declaredFields.length > 0) {
for (Field field : declaredFields) {
if (field.isAnnotationPresent(EncryptField.class) && field.getType().toString().endsWith("String")) {
field.setAccessible(true);
String fieldValue = (String)field.get(t);
if(StringUtils.isNotEmpty(fieldValue)) {
field.set(t, AESUtil.encrypt(fieldValue));
}
}
}
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
/**
* 加密单独的字符串
*
* @param @param t
* @param @return
* @return T
* @throws
*/
public static <T> T encryptStr(T t) {
if (t instanceof String) {
t = (T) AESUtil.encrypt((String) t);
}
return t;
}
/**
* 对含注解字段解密
*
* @param t
* @param <T>
*/
public static <T> T decrypt(T t) {
if (isEncryptAndDecrypt(t)) {
Field[] declaredFields = t.getClass().getDeclaredFields();
try {
if (declaredFields != null && declaredFields.length > 0) {
for (Field field : declaredFields) {
if (field.isAnnotationPresent(DecryptField.class) && field.getType().toString().endsWith("String")) {
field.setAccessible(true);
String fieldValue = (String) field.get(t);
if (StringUtils.isNotEmpty(fieldValue)) {
field.set(t, AESUtil.decrypt(fieldValue));
}
}
}
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
return t;
}
/**
* 对含注解字段解密
* @param t
* @param <T>
*/
public static <T> void decryptField(T t) {
Field[] declaredFields = t.getClass().getDeclaredFields();
try {
if (declaredFields != null && declaredFields.length > 0) {
for (Field field : declaredFields) {
if (field.isAnnotationPresent(DecryptField.class) && field.getType().toString().endsWith("String")) {
field.setAccessible(true);
String fieldValue = (String)field.get(t);
if(StringUtils.isNotEmpty(fieldValue)) {
field.set(t, AESUtil.decrypt(fieldValue));
}
}
}
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
// return t;
}
public static Object getDecryptMapValue(Object returnValue, String[] fields){
return null;
}
public static Object getEncryptMapValue(Object parameter, String[] fields) {
Map<String,Object> map = null;
try {
map = (Map<String, Object>) parameter;
} catch (Exception e) {
return parameter;
}
for (String field : fields) {
if (null == map.get(field)) continue;
if (map.get(field) instanceof String) {
String value = String.valueOf(map.get(field));
if (Strings.EMPTY.equals(value)) continue;
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (value.equals(entry.getValue())) {
map.put(entry.getKey(), AESUtil.encrypt(value));
}
}
}
}
return map;
}
/**
* 判断是否需要加密解密的类
*
* @param @param t
* @param @return
* @return Boolean
* @throws
*/
public static <T> Boolean isEncryptAndDecrypt(T t) {
return true;
}
/**
* 隐藏号码中间4位
* @param t
* @param <T>
*/
public static <T> void hidePhone(T t) {
Field[] declaredFields = t.getClass().getDeclaredFields();
try {
if (declaredFields != null && declaredFields.length > 0) {
for (Field field : declaredFields) {
if (field.isAnnotationPresent(DecryptField.class) && field.getType().toString().endsWith("String")) {
field.setAccessible(true);
String fieldValue = (String)field.get(t);
if(StringUtils.isNotEmpty(fieldValue)) {
// 暂时与解密注解共用一个注解,该注解隐藏手机号中间四位
field.set(t, StringUtils.overlay(fieldValue, "****", 3, 7));
}
}
}
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
(注) 将拦截加入配置,不加不生效
@Bean
public String myInterceptor(SqlSessionFactory sqlSessionFactory) {
sqlSessionFactory.getConfiguration().addInterceptor(new DataInterceptor());
return "myInterceptor";
}
四、使用如下
1.实体使用:在属性上加上加密和解密注解
2.单个入参数使用:在单个参数上加上参数
3.多个入参数使用,按照顺序加入记住,这块没有想好怎么实现
结束~~~~~~~,希望能够给到你帮助!!!
2021-08-18 11:32:38
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战