MyBatis实现字段加解密的实践
作者:十年培训经验的菜包
背景
互联网系统充斥着各种敏感信息,包括各种个人信息、商业信息等等。按照要求,不允许隐私信息明文存储,需要进行加密处理,防止造成隐私泄露的风险。
我司作为一个跨境电商公司,各系统中,自然免不了涉及各类敏感信息,并且对各类敏感信息的安全级别进行了划分,不同等级的加密要求级别有一定的差别。
方案
由于不同的敏感数据需要使用不同的加解密策略,在MySQL层面,无法满足需求,所以只能在应用代码层面进行实现。但需要考虑几个点
- 尽可能减少对业务代码侵入性;
- 以最小的风险进行改动;
- 方案可复用,方便拓展;
手动加解密
这是首先被提出来的方案。该方案做法是
- 在任何涉及到读取敏感字段的业务代码中,进行手动解密操作。
- 在任何涉及到写入敏感字段的业务代码中,进行手动加密操作。
优点:
- 暂无;
缺点:
- 侵入业务代码,业务开发甚至需要关注不同级别的加解密策略;
- 老旧系统调用复杂,可能出现重复加密或重复解密,导致无法复原原始数据,风险数据非常高;
- 若数据安全级别变动,加密策略升级,所有可能涉及到的业务代码都需要变更,风险半径无法预测;
综上所述,该方案完全无法满足我们对方案的要求,属于最笨的方案,可以直接say no。
自动加解密
由于无法在MySQL层面实现,又希望尽可能减少对业务代码的侵入性,那么任务只能落在ORM框架或半ORM框架上。我们系统使用的是MyBatis,那么利用MyBatis的插件机制来实现自动加解密,是个不错的选择。
优点:
- 业务代码无需改造,几乎对业务代码无入侵性;
- 加解密统一入口,风险可控;
- 方案多个系统通用,加解密策略可随时拓展;
缺点:
- 暂无;
实现
编码
由于敏感数据被划分成多个不同级别,各个级别使用的加解密算法不同,所以面对这种不同加密算法的场景,策略模式非常适合;
加解密策略
public interface SensitiveStrategy { /** * 加密 */ String encrypt(String value); /** * 解密 */ String decrypt(String value); }
各种加解密算法,只要实现该接口即可,此处略。
自定义注解
自定义一个字段上的注解,目的是为了让MyBatis拦截器识别哪些字段需要加解密,加解密的策略是什么。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) public @interface SensitiveField { Class<? extends SensitiveStrategy> sensitiveStrategy(); }
MyBatis拦截器
我们需要定义一个MyBatis拦截器,该拦截器的作用有以下:
- 拦截入参,识别被@SensitiveField注解的字段,并使用指定加密策略,对字段内容进行加密;
- 拦截查询结果,识别被@SensitiveField注解的字段,并使用指定的解密策略,对字段内容进行解密;
我们知道,MyBatis的拦截器插件,可以对四大组件Executor、StatementHandler、ParameterHandler、ResultSetHandler进行拦截,由于我们只需要对入参和结果进行拦截和修改,所以只需指定拦截ParameterHandler、ResultSetHandler即可。
@Slf4j @Component @Intercepts(value = { @Signature(type = ParameterHandler.class,method = "setParameters",args = {PreparedStatement.class}), @Signature(type = ResultSetHandler.class,method = "handleResultSets",args = {Statement.class}) }) public class SensitiveInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Object target = invocation.getTarget(); //入参加密 if (target instanceof ParameterHandler parameterHandler){ //获取参数对象 Object parameterObj = ReflectUtil.getFieldValue(parameterHandler, "parameterObject"); if (parameterObj != null){ //获取参数对象内的字段 Arrays.stream(ReflectUtil.getFields(parameterObj.getClass())) .filter(field -> String.class.equals(field.getType())) .filter(field -> field.getAnnotation(SensitiveField.class)!=null ) .filter(field -> ReflectUtil.getFieldValue(parameterObj,field) != null) .forEach(field -> { SensitiveField sensitiveField = field.getAnnotation(SensitiveField.class); Class<? extends SensitiveStrategy> strategyClazz = sensitiveField.sensitiveStrategy(); if (strategyClazz != null){ SensitiveStrategy strategy = SpringContext.getBean(strategyClazz); String encrypt = strategy.encrypt(ReflectUtil.getFieldValue(parameterObj, field).toString()); ReflectUtil.setFieldValue(parameterObj,field,encrypt); } }); ReflectUtil.setFieldValue(parameterHandler,"parameterObject",parameterObj); } } Object resultObj = invocation.proceed(); //出参解密 if (resultObj != null && target instanceof ResultSetHandler){ List<?> resultList = (List<?>) resultObj; for (Object result : resultList) { if (!SimpleTypeRegistry.isSimpleType(result.getClass())){ Arrays.stream(ReflectUtil.getFields(result.getClass())) .filter(field -> String.class.equals(field.getType())) .filter(field -> field.getAnnotation(SensitiveField.class)!=null ) .filter(field -> ReflectUtil.getFieldValue(result,field) != null) .forEach(field -> { SensitiveField sensitiveField = field.getAnnotation(SensitiveField.class); Class<? extends SensitiveStrategy> strategyClazz = sensitiveField.sensitiveStrategy(); if (strategyClazz != null){ SensitiveStrategy strategy = SpringContext.getBean(strategyClazz); String decrypt = strategy.decrypt(ReflectUtil.getFieldValue(result, field).toString()); ReflectUtil.setFieldValue(result,field,decrypt); } }); } } resultObj = resultList; } return resultObj; } @Override public Object plugin(Object target) { return Plugin.wrap(target,this); } }
由于系统是基于SpringBoot的,所以我们将所有实现的加解密策略对象,交给Spring容器管理。在MyBatis拦截器中,直接从Spring容器中获取对应加解密策略使用接口。
上线
上线阶段,我们分为以下几个步骤实施
- 临时关闭敏感数据列的修改功能入口,避免出现备份后源数据变更;
- 备份敏感字段列数据,小表DBA直接备份,大表编写代码分批备份;
- 利用apollo控制读取时不解密,写入时加密,对对应字段进行一次读取后更新即可完成加密;
- 观察涉及的业务功能读取是否正常;
- 开启敏感数据修改功能入口,观察系统是否正常;
其他问题
其实在整个过程中,实现功能相关的编码并不复杂。除此之外,需要去识别并解决其他的一些问题,这期间花费了更多的时间,例如
敏感字段like搜索
字段加密后存储,就无法直接使用like关键字搜索。其实经过我司安全部门评估,敏感字段也不允许进行模糊搜索,有数据泄露风险,所以在产品方案层面,直接砍掉了类似功能;
若是一定要保留改功能,也可以使用以下方案
- 拓展一个新字段,用于模糊搜索,类型为text;
- 对原始字符进行分割,可按每N个字符为一项进行分割,分割后每一项使用固定加密算法加密,再使用固定字符对每一项进行拼接,形成新的字符串,保存到新字段;
- like查询时,同样也是分割、加密、拼接的方式;
注意,like字段需要使用text类型,性能会很低。
敏感字段group by
对于一些敏感级别较低的字段,采用了固定加密方式(即多次对相同的数据进行加密后结果不变),此时由于结果不变,可以直接使用加密后的字段进行group by;
但是对于敏感级别较高的字段,我司采用了动态加密方式(即多次对相同的数据加密结果不一致),此时由于结果不一致,无法进行group by操作。解决方案的拓展多一列,对源数据使用固定加密后,将结果存进该拓展字段,group by业务使用。
到此这篇关于MyBatis实现字段加解密的实践的文章就介绍到这了,更多相关MyBatis 字段加解密内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!