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和偏移量)
* @Date 2019-12-24
*/
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> * @author Yampery * @date 2017/10/24 13:05 */ @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> * @author Yampery * @date 2017/10/24 13:01 */ @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> * @author cheng * @date 2021/08/17 13:36 */ 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