一:背景
今天,公司新要求对数据库的敏感数据进行加密,对手机号、身份证号、姓名等一些敏感数据进行加密处理,要求:
1. 通过程序实现数据加密解密 (快速便捷,尽量减少对原先代码的修改)
2. 可以通过sql查询对数据进行解密(通过密钥直接对数据库加密参数进行解密)
3. 数据库:mysql
二:思考
1. 需要用到sql对数据库进行直接解密,同时又比较安全,第一个想到的是使用mysql自带的AES对数据进行加密
2. 对代码减少修改,想到使用mybatis拦截器实现,对 sql语句 或者 参数 进行修改
3. 数据只需要在入库层进行加解密,上层业务调用不需要再考虑敏感数据的加解密问题
三:mysql AES加解密
-- 加密 key: 123213 val: qwertyuiop123
select to_base64(AES_ENCRYPT('qwertyuiop123','123213'))
-- 返回:ZDuEEEeORjQFUlr7rkTsQg==
-- 解密:key: ZDuEEEeORjQFUlr7rkTsQg== val: qwertyuiop123
SELECT CONVERT(AES_DECRYPT(from_base64('ZDuEEEeORjQFUlr7rkTsQg=='), '123213') USING UTF8MB4)
-- 返回:qwertyuiop123
四:实现一: 修改sql语句实现入库加密
实现方式:
@Component
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class InsuranceMybatisPlugin implements Interceptor {
private String myBatisAESKey = "123123";
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
// 获取要执行的 sql
BoundSql boundSql = statementHandler.getBoundSql();
String sqlBefore = boundSql.getSql();
// 判断当前sql 是否需要加密
boolean containsChannel = sqlBefore.contains("{{myBatisAESKey}}");
// 替换加密需要的key
if(StringUtils.isNotBlank(sqlBefore) && containsChannel){
// 替换key值
String sqlAfter = sqlBefore.replaceAll("\\{\\{myBatisAESKey\\}\\}", myBatisAESKey );
Field sqlField = boundSql.getClass().getDeclaredField("sql");
// 重新设置对应sql
sqlField.setAccessible(true);
ReflectionUtils.setField(sqlField, boundSql, sqlAfter);
sqlField.setAccessible(false);
}
}catch (Exception e){
// 打印错误日志
}
return invocation.proceed();
}
}
-- xml sql加密修改
select 加密字段
-- 改为:
select SELECT to_base64(AES_ENCRYPT(加密字段,'{{myBatisAESKey}}'))
-- xml sql 解密修改
select 解密字段
--改为:
select convert(AES_DECRYPT(from_base64(解密字段), '{{myBatisAESKey}}') using UTF8MB4)
优点:
1. 实现简单,只需要不到20行代码就能实现
2. 可以使用mysql自带的解密
3. 效率比较高,一般sql语句都不会太长,替换比较快
缺点:
1. 需要修改原先写好的xml sql, 如果项目比较大的话工作量也不怎么轻松
2. 后续如果换数据库的话,又需要重新更改sql
3. 系统需要重新测试,防止人为因素导致sql修改错误(粗心导致的sql异常)
五:实现二:mybatis拦截器 + 注解
1. 需要解决的问题:
1. 程序加密后 可以使用 mysql 自带的函数进行解密
2. 不修改原先sql, 就能实现加密
3. 降低对程序的影响,减少代码侵入
2. 解决加解密问题
mysql 使用AES加密,程序我也可以用同样的加解密方式,这不就没问题了,代码实现:
public class EncryptUtil {
private static final Logger log = LoggerFactory.getLogger(EncryptUtil.class);
/**
* 加密算法
*/
private static final String KEY_ALGORITHM = "AES";
/**
* 算法/模式/补码方式
*/
private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
/**
* 编码格式
*/
private static final String CODE = "utf-8";
/**
* base64验证规则
*/
private static final String BASE64_RULE = "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)=?$";
/**
* 正则验证对象
*/
private static final Pattern PATTERN = Pattern.compile(BASE64_RULE);
/**
* 加密
* @param content 加密参数
* @param key 加密key
* @return 结果字符串
*/
public static String encrypt(String content, String key) {
// 判断如果已经是base64加密字符串则返回原字符串
if (Objects.isNull(content) || isBase64(content)) {
return content;
}
byte[] encrypted = encrypt2bytes(content, key);
if (null == encrypted || encrypted.length < 1) {
return null;
}
// 转成 base64
return Base64Utils.encodeToString(encrypted);
}
/**
* @param content 加密字符串
* @param key 加密key
* @return 返回加密字节
*/
public static byte[] encrypt2bytes(String content, String key) {
try {
byte[] raw = key.getBytes(CODE);
SecretKeySpec secretKeySpec = new SecretKeySpec(raw, KEY_ALGORITHM);
Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
return cipher.doFinal(content.getBytes(CODE));
} catch (Exception e) {
log.error("encrypt2bytes to content: {}, key {}", content, key, e);
return null;
}
}
/**
* 解密
*
* @param content 解密字符串
* @param key 解密key
* @return 解密结果
*/
public static String decrypt(String content, String key) {
// 不是base64格式字符串则不进行解密
if (!isBase64(content)) {
return content;
}
// 先转成 Base64 再进行解密
return decrypt(Base64Utils.decodeFromString(content), key);
}
/**
* @param content 解密字节
* @param key 解密key
* @return 返回解密内容
*/
public static String decrypt(byte[] content, String key) {
if (key == null) {
return null;
}
try {
byte[] raw = key.getBytes(CODE);
SecretKeySpec keySpec = new SecretKeySpec(raw, KEY_ALGORITHM);
Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] original = cipher.doFinal(content);
return new String(original, CODE);
} catch (Exception e) {
log.error("failed to decrypt content: {} key: {}", content, key, e);
return null;
}
}
/**
* 判断是否为 base64加密
*
* @param str 参数
* @return 结果
*/
public static boolean isBase64(String str) {
Matcher matcher = PATTERN.matcher(str);
return matcher.matches();
}
/**
* 加密 解密实验
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
System.out.println(EncryptUtil.encrypt("138094asdas49025", "qwertyuieqweqw12"));
System.out.println(EncryptUtil.decrypt("BQOBvTgoRzEhHuNwgn1llTfVcvpwHahOzd5otNKlsRY=", "qwertyuieqweqw12"));
// 执行结果为:
// 加密: BQOBvTgoRzEhHuNwgn1llTfVcvpwHahOzd5otNKlsRY=
// 解密: 138094asdas49025
}
mysql 加解密:
select to_base64(AES_ENCRYPT('138094asdas49025','qwertyuieqweqw12'))
select convert(AES_DECRYPT(from_base64('BQOBvTgoRzEhHuNwgn1llTfVcvpwHahOzd5otNKlsRY='), 'qwertyuieqweqw12') using UTF8MB4)
-- 执行结果
-- 加密:BQOBvTgoRzEhHuNwgn1llTfVcvpwHahOzd5otNKlsRY=
-- 解密:138094asdas49025
3. 实现参数加密,入参加密:
方法标识SensitiveMethodParam 注解
/**
* 方法标识,入参加密
*/
@Inherited
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveMethodParam {
}
参数标识SensitiveParam注解,具体加密的字段
@Documented
@Inherited
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveParam {
}
拦截器实现:
@Component
@Intercepts(
{
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@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, CacheKey.class, BoundSql.class}),
}
)
public class BarExecuteInterceptor implements Interceptor {
private static final Logger log = LoggerFactory.getLogger(BarExecuteInterceptor.class);
private final String AES_KEY = "qwertyuiopasdfgh";
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取当前方法
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
// 方法为空, 正常执行
Method interfaceMethod = InterceptorUtil.getInterfaceMethod(mappedStatement.getId());
Object parameterObject = invocation.getArgs()[1];
// 一个参数的情况下 直接放入当前参数,多个参数的情况下 ParamMap 将结果进行包装
// 存在 param 注解 key 为 param
// 验证是否存在加密参数方法
if (Objects.isNull(interfaceMethod) || Objects.isNull(parameterObject) || isNotSensitiveMethodParam(interfaceMethod))
return invocation.proceed();
// 获取加密的参数名称,多个参数
Set<String> sensitiveNameSet = getSensitiveNameSet(interfaceMethod);
// 多个参数 mybatis 使用 MapperMethod.ParamMap进行包装
sensitiveParam(invocation, sensitiveNameSet, true);
// 重新生成动态参数, list<Sting>集合的动态参数会被解析出来,所以需要重新生成一下参数
setBoundSql(mappedStatement, invocation.getArgs());
// 执行完成后将参数进行解密,防止影响后续流程,
Object result;
try {
result = invocation.proceed();
} finally {
sensitiveParam(invocation, sensitiveNameSet, false);
}
// 返回执行结果
return result;
}
/**
* 验证当前方法是否为加密参数方法
*
* @param interfaceMethod MappedStatement 对应方法
* @return
*/
private boolean isNotSensitiveMethodParam(Method interfaceMethod) {
SensitiveMethodParam annotation = interfaceMethod.getAnnotation(SensitiveMethodParam.class);
return Objects.isNull(annotation);
}
/**
* 获取方法中添加了注解的方法
*
* @param method
* @return
*/
private Set<String> getSensitiveNameSet(Method method) {
Set<String> sensitiveNames = new HashSet<>();
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
Annotation[] annotations = method.getParameterAnnotations()[i];
// 验证参数 是否存在 SensitiveMethodParam 注解
boolean noneMatch = Arrays.stream(annotations)
.noneMatch(annotation -> annotation instanceof SensitiveParam);
if (noneMatch) continue;
// 获取 paramMap 的 key, Param注解 > 形参名称
String name = Arrays.stream(annotations)
.filter(annotation -> annotation instanceof Param)
.map(annotation -> ((Param) annotation).value())
.findFirst()
.orElse(parameter.getName());
sensitiveNames.add(name);
}
return sensitiveNames;
}
/**
* 加解密参数
* @param invocation 对象
* @param sensitiveNameSet 加密参数
* @param isEncrypt 加解密标识
*/
private void sensitiveParam(Invocation invocation,Set<String> sensitiveNameSet, boolean isEncrypt){
if (invocation.getArgs()[1] instanceof MapperMethod.ParamMap) {
@SuppressWarnings("unchecked")
Map<String, Object> stringObjectMap = (Map<String, Object>) invocation.getArgs()[1];
sensitiveParamMap(stringObjectMap, sensitiveNameSet, isEncrypt);
return;
}
invocation.getArgs()[1] = sensitiveParamOther(invocation.getArgs()[1], sensitiveNameSet, isEncrypt);
}
/**
* 处理 类型为 paramMap
* @param parameterObject
* @param sensitiveNameSet
*/
private void sensitiveParamMap(Map<String, Object> parameterObject, Set<String> sensitiveNameSet, boolean isEncrypt) {
for (String key : sensitiveNameSet) {
// 获取需要加密的 value
Object value = parameterObject.get(key);
// 值为字符串
if (value instanceof String) {
parameterObject.put(key, sensitiveString((String) value, isEncrypt));
}
// 值为集合, 没有处理集合为不可变类型 如: Collections.singletonList()
else if(value instanceof List) {
@SuppressWarnings("unchecked")
List<Object> list = (List<Object>) value;
sensitiveList(list, isEncrypt);
}
// 其他类型处理
else {
sensitiveObject(value, isEncrypt);
}
}
}
/**
* 处理其他类型,直接只有一个参数类型,没有进行 paramMap包装,所以需要单独解析
* @param parameterObject
* @param sensitiveNameSet
* @return
*/
private Object sensitiveParamOther(Object parameterObject, Set<String> sensitiveNameSet, boolean isEncrypt) {
// 只有一个形参的情况下 没有加密注解 直接跳过
if (sensitiveNameSet.isEmpty())
return parameterObject;
if (parameterObject instanceof String) {
return sensitiveString((String) parameterObject, isEncrypt);
}
// 值为集合, 没有处理集合为不可变类型 如: Collections.singletonList()
else if (parameterObject instanceof List) {
@SuppressWarnings("unchecked")
List<Object> list = (List<Object>) parameterObject;
sensitiveList(list, isEncrypt);
}
// 处理其他类型,
else {
sensitiveObject(parameterObject, isEncrypt);
}
return parameterObject;
}
/**
* 加密 list
* @param list
*/
private void sensitiveList(List<Object> list, boolean isEncrypt) {
for (int i = 0; i < list.size(); i++) {
Object val = list.get(i);
if (val instanceof String) {
list.set(i, sensitiveString((String) val, isEncrypt));
} else {
sensitiveObject(val, isEncrypt);
}
}
}
/**
* 加密普通对象
* @param parameterObject
*/
private void sensitiveObject(Object parameterObject, boolean isEncrypt) {
// 过滤调一些不需要加密的类型,
if (parameterObject instanceof Map || parameterObject instanceof Collection
|| parameterObject instanceof Number || parameterObject instanceof Boolean
|| parameterObject instanceof String || parameterObject instanceof Date)
return;
// 如果是普通对象,检查是否有@SensitiveField注解的字段
// 只处理普通对象
Class<?> clazz = parameterObject.getClass();
for (Field field : clazz.getDeclaredFields()) {
if (Objects.isNull(field.getAnnotation(SensitiveParam.class))) // 验证数据是否需要加密
continue;
try {// map 和 其他类型 不进行处理
field.setAccessible(true);
Object value = field.get(parameterObject);
if (value instanceof String) {
field.set(parameterObject, sensitiveString((String) value, isEncrypt));
} else if (value instanceof List) {
@SuppressWarnings("unchecked")
List<Object> list = (List<Object>) value;
sensitiveList(list, isEncrypt);
} else {
sensitiveObject(value, isEncrypt);
}
} catch (Exception e){
log.error("数据加密异常!", e);
} finally {
field.setAccessible(false);
}
}
}
/**
*
* @param value 加解密字符串
* @param isEncrypt true(加密) false(解密)
* @return
*/
private String sensitiveString(String value, boolean isEncrypt) {
return isEncrypt ? EncryptUtil.encrypt(value, AES_KEY)
: EncryptUtil.decrypt(value, AES_KEY);
}
/**
* 处理动态参数
* @param mappedStatement
* @param args
*/
private void setBoundSql(MappedStatement mappedStatement, Object[] args) {
if(args.length == 6 && Objects.nonNull(args[5])){
args[5] = mappedStatement .getBoundSql(args[1]);
}
}
4. 实现返回参数解密:
方法标识 SensitiveMethodResult注解
/**
* 方法标识 返参解密
* 当方法添加此注解 返参参数为 string 或者 list<String> 是会对内部参数进行解密
* 当返参参数为普通对象时,只有内部属性添加 SensitiveResult 注解的 才会进行解密
*/
@Inherited
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveMethodResult {
}
返回参数标识SensitiveResult注解
@Documented
@Inherited
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveResult {
}
拦截器实现:
@Component
@Intercepts(
{
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@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, CacheKey.class, BoundSql.class}),
}
)
public class RetExecuteInterceptor implements Interceptor {
private static final Logger log = LoggerFactory.getLogger(RetExecuteInterceptor.class);
private final String AES_KEY = "qwertyuiopasdfgh";
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 执行方法, 获取当前方法
Object proceed = invocation.proceed();
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
Method interfaceMethod = InterceptorUtil.getInterfaceMethod(mappedStatement.getId());
if(Objects.isNull(proceed) || Objects.isNull(interfaceMethod) || isNotSensitiveMethodResult(interfaceMethod))
return proceed;
// 处理返回类型, 返回类型只有 集合 和 数字, 数字不处理 只处理集合
if(proceed instanceof ArrayList) {
@SuppressWarnings("unchecked")
ArrayList<Object> resultList = (ArrayList<Object>) proceed;
for (int i = 0; i < resultList.size(); i++) {
Object obj = resultList.get(i);
// 为空数据直接跳过
if(Objects.isNull(obj))
continue;
// 字符串直接解密
if(obj instanceof String) {
resultList.set(i, EncryptUtil.decrypt((String) obj, AES_KEY));
continue;
}
decryptObject(obj);
}
}
return proceed;
}
/**
* 验证当前方法是否为加密参数方法
*
* @param interfaceMethod
* @return
*/
private boolean isNotSensitiveMethodResult(Method interfaceMethod) {
SensitiveMethodResult annotation = interfaceMethod.getAnnotation(SensitiveMethodResult.class);
return Objects.isNull(annotation);
}
/**
* 处理对象类型
* @param obj 解密的对象
*/
private void decryptObject(Object obj){
// 过滤调一些不需要解密的类型,防止类型错误导致死循环
if (obj instanceof Map || obj instanceof Collection || obj instanceof Number ||
obj instanceof Boolean || obj instanceof String || obj instanceof Date)
return;
// 如果是普通对象,检查是否有 @SensitiveResult 注解的字段
Class<?> clazz = obj.getClass();
for (Field field : clazz.getDeclaredFields()) {
// 验证数据是否需要加密
if (Objects.isNull(field.getAnnotation(SensitiveResult.class)))
continue;
try {// 只处理字符串类型
field.setAccessible(true);
Object value = field.get(obj);
if (value instanceof String) {
field.set(obj, EncryptUtil.decrypt((String) value, AES_KEY));
}
} catch (Exception e){
log.error("数据加密异常!", e);
} finally {
field.setAccessible(false);
}
}
}
InterceptorUtil实现:
public class InterceptorUtil {
private static final Logger log = LoggerFactory.getLogger(InterceptorUtil.class);
/**
* 获取 接口方法
*
* @param mappedStatement mybatis 拦截器 Interceptor 参数
* @return 接口方法
*/
public static Method getInterfaceMethod(String statementId) {
// 获取MappedStatement的 id,格式为namespace.id
try {
// 获取 namespace 和 id
int lastDot = statementId.lastIndexOf(".");
String namespace = statementId.substring(0, lastDot);
String methodId = statementId.substring(lastDot + 1);
Class<?> mapperInterface = Class.forName(namespace);// 获取接口类型
for (Method method : mapperInterface.getDeclaredMethods()) {// 获取接口方法
if (method.getName().equals(methodId))
return method;
}
} catch (Exception e) {
log.error("方法获取失败,获取属性: {}", statementId);
}
return null;
}
5. 案例:
@SensitiveMethodParam
int insertEdcMonitorLog(@SensitiveParam EdcMonitorLog edcMonitorLog);
@SensitiveMethodParam
int insertEdcMonitorLog2(@SensitiveParam List<String> edcMonitorLog);
@SensitiveMethodParam
int insertEdcMonitorLog3(@SensitiveParam List<EdcMonitorLog> edcMonitorLog);
@SensitiveMethodParam
@SensitiveMethodResult
List<EdcMonitorLog> selectEdcMonitorLog(@SensitiveParam String msgId);
@SensitiveMethodParam
@SensitiveMethodResult
List<EdcMonitorLog> selectEdcMonitorLog2(@SensitiveParam List<String> msgId);
@SensitiveMethodParam
@SensitiveMethodResult
List<EdcMonitorLog> selectEdcMonitorLog3(@SensitiveParam List<EdcMonitorLog> list);
@SensitiveMethodParam
@SensitiveMethodResult
List<String> selectMsgId(@SensitiveParam @Param("list1") List<String> list1, @Param("list2") List<String> list2);
@Data
public class EdcMonitorLog {
@SensitiveParam
@SensitiveResult
private String msgId;
}
优点:
1. 对代码没有侵入,对sql也没有修改
2. 可以通过数据库对参数进行直接解密
缺点:
1. 如果参数比较复杂的情况下,多级嵌套,可能影响效率
2. 获取方法没有进行优化(getInterfaceMethod) 可以缓存解析结果,本人偷个懒,期待大佬帮忙优化一下
3. 普通对象需要加密的字段也可以缓存(期待大佬优化)
4. 当前只加密了字符串类型,其他类型没有处理(没有需求)
5. 程序加密的AES算法要求的密钥长度必须是128, 192或256位,但是使用mysql是没有影响的
六:mybatis插件原理
Mybatis的插件,是采用责任链机制,通过JDK动态代理来实现的。默认情况下,Mybatis允许使用插件来拦截四个对象:
Executor:执行CURD操作;
StatementHandler:处理sql语句预编译,设置参数等相关工作;
ParameterHandler:设置预编译参数用的;
ResultSetHandler:处理结果集。
方案一:拦截预编译sql
方案二:拦截执行器 query、update方法
标签:拦截器,return,String,param,敏感数据,key,Mybatis,加密,class From: https://blog.csdn.net/weixin_43262384/article/details/142627898