企业级数据脱敏方案!

最近几年经常发生用户数据泄漏的事件,给企业带来危机。随着用户对个人隐私数据的重视和法律法规的完善,数据安全显得愈发重要。一方面可以加强权限管理,减少能够接触数据的人员以及导出数据加强审批。另一方面,还需要从技术上对用户隐私数据进行脱敏处理,提高数据的安全性。

数据脱敏方法有很多种,大致可以按照以下进行分类:

隐藏法: 只显示敏感信息的部分内容,其他部分进行遮挡,比较常见使用星号替代。这种方式日常比较多见,比如手机号,银行卡号等只显示后面和后面几位,好处是虽然只是部分内容显示,但足够提供有效信息,同时不会暴露完整数据。混淆法: 对原有数据截断、替换、隐藏、数字进行随机移位,使得原有数据完全失真或者部分失真,混淆真假。加密: 通过加密密钥和算法对敏感数据进行加密得到密文,密文可见但是完全没有可读意义,是脱敏最彻底的方法。其中对称加密还能密钥解密可以从密文恢复原始数据。比如密码保存采用非对称加密,手机号存储时采用对称加密。

用户的敏感数据包含姓名、电话号码、身份证、银行卡号、电子邮件、家庭住址、登录密码等等。需要考虑数据的敏感程度、数据安全要求以及实际业务使用场景选择合适的脱敏方法。Hutool包里面提供了许多常用的脱敏方法。

企业脱敏方案

企业如何实现脱敏?我们先来看典型的系统数据交互链路,数据需要经过数据库、后端应用、app端。

图片

数据库侧: 数据库保存了原始数据,有权限人员可以查看数据和导出数据。后端应用内: 后端应用中会打印相关日志,数据通过日志得到了存储下来。通过日志,能够得到原始数据。应用输出: app侧能够从后端读取到原始数据。

数据库脱敏方案

数据库脱敏方法根据业务具体要求选择合适脱敏方法。脱敏地点可以在应用中手动脱敏,当然这种方法不常用,改动点多对业务侵入大。

另外一种方案在ORM框架中修改sql实现,其中mybatis框架为java后端系统中最常用的框架。mybatis自带拦截器扩展,允许在映射语句执行过程中的某一点进行拦截调用。关注公众号:码猿技术专栏,回复关键词:1111 获取阿里内部Java性能调优手册!默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

Executor: 拦截执行器的方法,例如 update、query、commit、rollback 等。可以用来实现缓存、事务、分页等功能。ParameterHandler: 拦截参数处理器的方法,例如 setParameters 等。可以用来转换或加密参数等功能。ResultSetHandler: 拦截结果集处理器的方法,例如 handleResultSets、handleOutputParameters 等。可以用来转换或过滤结果集等功能。StatementHandler: 拦截语句处理器的方法,例如 prepare、parameterize、batch、update、query 等。可以用来修改 SQL 语句、添加参数、记录日志等功能。

Mybatis执行流程

数据库脱敏另外一个问题是历史数据问题。历史原因最开始的技术方案保存明文,所以脱敏时需要做到平滑脱敏。要做到平滑脱敏,可按照如下流程:

新增脱敏字段: 在源表上新增脱敏字段。数据双写: 源字段和脱敏字段都写入数据。历史数据迁移: 历史数据迁移,刷入脱敏字段。读切换脱敏字段: 从脱敏字段读取数据返回。清空源字段: 确保所有流程都正确的情况下,清空源字段。

本文mybatis实现数据库加解密为例。

1.表里面新增脱敏字段,示例中脱敏新字段格式规范为源字段添加encrypt后缀。vo里面添加脱敏注解标记。

复制
public class Employee { private Long id; private String name; @EncryptTag private String mobile; private String mobileEncrypt; private String email; private double salary; }1.2.3.4.5.6.7.8.9.10.11.

2.实现自定义拦截。

复制
/*** ** 加密拦截 ***/ @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, CacheKey.class, BoundSql.class} ), @Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} )}) public class EncryptPlugin implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement mappedStatement = (MappedStatement)invocation.getArgs()[0]; Object param = invocation.getArgs()[1]; PluginService.encrypt(invocation, param); return invocation.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } } /*** ** 解密拦截 ***/ @Intercepts({@Signature( type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class} )}) public class DecryptPlugin implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Object result = invocation.proceed(); if (result != null && result instanceof List) { this.decrypt(((List) result).iterator()); } return result; } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } private void decrypt(Iterator iterator) throws Throwable { while(iterator.hasNext()) { Object object = iterator.next(); PluginService.decrypt(object); } } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.76.

3.实现sql修改,完成加解密逻辑。

复制
public class PluginService { privatestaticfinal Logger LOGGER = LoggerFactory.getLogger(PluginService.class); privatestaticfinal Map<String, List<Field>> ENCRYPT_TAG_FIELDS = new ConcurrentHashMap(); public static void encrypt(Invocation invocation, Object object) throws Throwable { if (object.getClass().isArray()) { int length = Array.getLength(object); if (length <= 0) { return; } for (int i = 0; i < length; ++i) { encryptSingleObject(Array.get(object, i)); } } elseif (object instanceof Collection) { Collection collection = (Collection) object; Iterator itr = collection.iterator(); while (itr.hasNext()) { Object item = itr.next(); encryptSingleObject(item); } } else { encryptSingleObject(object); } } private static void encryptSingleObject(Object object) throws Throwable { if (object != null) { String className = object.getClass().getName(); List<Field> EncryptTagFields = ENCRYPT_TAG_FIELDS.get(className); if (EncryptTagFields == null) { EncryptTagFields = findEncryptTagFields(object); ENCRYPT_TAG_FIELDS.putIfAbsent(className, EncryptTagFields); } encryptFields(object, EncryptTagFields); } } private static void encryptFields(Object object, List<Field> EncryptTagFields) throws Throwable { if (object != null && !EncryptTagFields.isEmpty()) { String[] originalValues = new String[EncryptTagFields.size()]; for(int i = 0; i < EncryptTagFields.size(); ++i) { Field field = (Field)EncryptTagFields.get(i); String value = (String)field.get(object); originalValues[i] = value; } for(int i = 0; i < EncryptTagFields.size(); ++i) { Field field = (Field)EncryptTagFields.get(i); String value = originalValues[i]; if (value == null) { continue; } Field encryptField = getEncryptField(object, field); if (encryptField == null) { continue; } String encryptValue = encryptFieldValue(value); encryptField.set(object, encryptValue); field.set(object, null); } } } private static String encryptFieldValue(String value) { String encryptValue = value + "encrypt"; return encryptValue; } public static void decrypt(Object object) throws Throwable { if (object == null) { return; } String className = object.getClass().getName(); List<Field> encryptTagFields = ENCRYPT_TAG_FIELDS.get(className); if (encryptTagFields == null) { encryptTagFields = findEncryptTagFields(object); ENCRYPT_TAG_FIELDS.putIfAbsent(className, encryptTagFields); } decryptFields(object, encryptTagFields); } private static void decryptFields(Object object, List<Field> encryptTagFields) throws Throwable { if (encryptTagFields.isEmpty()) { return; } for (int i = 0; i < encryptTagFields.size(); ++i) { Field field = encryptTagFields.get(i); Field encryptField = getEncryptField(object, field); Object fieldValue = encryptField.get(object); if (fieldValue == null) { continue; } if (fieldValue instanceof String) { String value = (String) fieldValue; value = AesUtil.decrypt(value); field.set(object, value); encryptField.set(object, null); } } } private static List<Field> findEncryptTagFields(Object object) { Class clazz = object.getClass(); List<Field> fieldList = new ArrayList<>(); for(; clazz != null; clazz = clazz.getSuperclass()) { Field[] declaredFields = clazz.getDeclaredFields(); int length = declaredFields.length; for(int index = 0; index < length; ++index) { Field field = declaredFields[index]; if (field.getAnnotation(EncryptTag.class) != null) { if (field.getType() == String.class) { field.setAccessible(true); fieldList.add(field); } else { LOGGER.error("@EncryptTag should be used on String field. class: {}, fieldName: {}", clazz.getName(), field.getName()); } } } } return fieldList; } private static Field getEncryptField(Object object, Field field) throws Exception { String encryptFieldName = AesUtil.encrypt(field.getName()); Field encyptField = getField(object, encryptFieldName); if (encyptField == null) { thrownew Exception(object.getClass() + "对象没有对应的加密字段:" + encryptFieldName); } else { encyptField.setAccessible(true); return encyptField; } } public static Field getField(Object object, String fieldName) { for(Class clazz = object.getClass(); clazz != null; clazz = clazz.getSuperclass()) { Field[] var3 = clazz.getDeclaredFields(); int var4 = var3.length; for(int var5 = 0; var5 < var4; ++var5) { Field field = var3[var5]; if (field.getName().equals(fieldName)) { return field; } } } returnnull; } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.76.77.78.79.80.81.82.83.84.85.86.87.88.89.90.91.92.93.94.95.96.97.98.99.100.101.102.103.104.105.106.107.108.109.110.111.112.113.114.115.116.117.118.119.120.121.122.123.124.125.126.127.128.129.130.131.132.133.134.135.136.137.138.139.140.141.142.143.144.145.146.147.148.149.150.151.152.153.154.155.156.157.158.159.160.161.

日志脱敏方案

日志脱敏,核心在于序列化时对于敏感字段修改其序列化方式。各大序列化工具一般都有序列化自定义功能,关注公众号:码猿技术专栏,回复关键词:IDEA 获取最新版IDEA破解脚本!本文以fastjson为例讲解实现,实现方式有两种:

基于注解@JSONField实现基于序列化过滤器

@JSONField方式不建议使用,对业务入侵太大。另外一种继续序列化过滤器,fastjson提供了多种SerializeFilter

PropertyPreFilter 根据PropertyName判断是否序列化PropertyFilter 根据PropertyName和PropertyValue来判断是否序列化NameFilter 修改Key,如果需要修改Key,process返回值则可ValueFilter 修改ValueBeforeFilter 序列化时在最前添加内容AfterFilter 序列化时在最后添加内容

通过实现ValueFilter自定义序列化扩展,针对目标类以及字段进行脱敏返回。

核心代码简化如下:

复制
public class FastjsonValueFilter implements ValueFilter { @Override public Object process(Object object, String name, Object value) { if (needDesensitize(object, name)) { return desensitize(value); } } } String s = JSON.toJSONString(new Person("131xxxx1552","123@163.com"),new FastjsonValueFilter());1.2.3.4.5.6.7.8.9.10.

在标记脱敏字段以及对应方法时,可以通过配置的方法, 对类相关的脱敏字段以及方法进行封装。要求不高的话添加响应的注解也可实现。

输出脱敏

在输出层织入切面进行拦截,在切面内实现脱敏逻辑。实现逻辑跟日志脱敏类似,需要对脱敏字段进行标记以及对应脱敏方法。

如果是Spring Boot集成,配置 Spring MVC 的话只需继承 WebMvcConfigurer 覆写 configureMessageConverters方法,支持全局和指定类脱敏配置,示例如下:

复制
@Configuration publicclass FastJsonWebSerializationConfiguration implements WebMvcConfigurer { @Bean(name = "httpMessageConverters") public HttpMessageConverters fastJsonHttpMessageConverters() { // 1.定义一个converters转换消息的对象 FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter(); // 2.添加fastjson的配置信息,比如: 是否需要格式化返回的json数据 FastJsonConfig fastJsonConfig = new FastJsonConfig(); fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat); // 中文乱码解决方案 List<MediaType> mediaTypes = new ArrayList<>(); //设定json格式且编码为UTF-8 mediaTypes.add(MediaType.APPLICATION_JSON_UTF8); fastConverter.setSupportedMediaTypes(mediaTypes); //添加全局自定义脱敏 fastJsonConfig.setSerializeFilters(new ValueDesensitizeFilter()); //添加指定类脱敏方法 Map<Class<?>, SerializeFilter> classSerializeFilters = new HashMap<>(); classSerializeFilters.put(Employee.class, new FastjsonValueFilter()); fastJsonConfig.setClassSerializeFilters(classSerializeFilters); // 3.在converter中添加配置信息 fastConverter.setFastJsonConfig(fastJsonConfig); // 4.将converter赋值给HttpMessageConverter HttpMessageConverter<?> converter = fastConverter; // 5.返回HttpMessageConverters对象 returnnew HttpMessageConverters(converter); } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.

总结

本文总结了企业中脱敏方案实现,包含数据库脱敏、日志脱敏、输出脱敏,并贴上关键实现代码。能够满足业务的要求。

阅读剩余
THE END