详解企业中数据脱敏方案实现

阅读说明:

  1. 如果有排版格式问题,请移步www.yuque.com/mrhuang-ire... 《详解企业中数据脱敏方案实现》,选择宽屏模式效果更佳。
  2. 本文为原创文章,转发请注明出处。如果觉得文章不错,请点赞、收藏、关注一下,您的认可是我写作的动力。

简介

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

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

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

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

企业脱敏方案

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

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

数据库脱敏方案

数据库脱敏方法根据业务具体要求选择合适脱敏方法。脱敏地点可以在应用中手动脱敏,当然这种方法不常用,改动点多对业务侵入大。另外一种方案在ORM框架中修改sql实现,其中mybatis框架为java后端系统中最常用的框架。mybatis自带拦截器扩展,允许在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

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

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

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

本文mybatis实现数据库加解密为例。 1.表里面新增脱敏字段,示例中脱敏新字段格式规范为源字段添加encrypt后缀。vo里面添加脱敏标记。

java 复制代码
public class Employee {

    private Long id;
    private String name;

    @EncryptTag
    private String mobile;
    private String mobileEncrypt;
    private String email;
    private double salary;
}    

2.实现自定义拦截。

java 复制代码
/***
** 加密拦截
***/
@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);
        }
    }
}

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

java 复制代码
public class PluginService {

    private static final Logger LOGGER = LoggerFactory.getLogger(PluginService.class);

    private static final 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));
            }
        } else if (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) {
            throw new 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;
                }
            }
        }

        return null;
    }
}

日志脱敏方案

日志脱敏,核心在于序列化时对于敏感字段修改其序列化方式。各大序列化工具一般都有序列化自定义功能,本文以fastjson为例讲解实现,实现方式有两种:

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

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

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

核心代码简化如下:

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

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

输出脱敏

在输出层织入切面进行拦截,在切面内实现脱敏逻辑。实现逻辑跟日志脱敏类似,需要对脱敏字段进行标记以及对应脱敏方法。如果是Spring Boot集成, 配置 Spring MVC 的话只需继承 WebMvcConfigurer 覆写 configureMessageConverters 方法, 支持全局和指定类脱敏配置,示例如下:

java 复制代码
@Configuration
public class 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对象
        return new HttpMessageConverters(converter);
    }
}

总结

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

参考: juejin.cn/post/703618... juejin.cn/post/730453... shardingsphere.apache.org/document/5.... www.cnblogs.com/zys2019/p/1... juejin.cn/post/690409... juejin.cn/post/706791...

相关推荐
tan180°6 小时前
MySQL表的操作(3)
linux·数据库·c++·vscode·后端·mysql
DuelCode7 小时前
Windows VMWare Centos Docker部署Springboot 应用实现文件上传返回文件http链接
java·spring boot·mysql·nginx·docker·centos·mybatis
优创学社27 小时前
基于springboot的社区生鲜团购系统
java·spring boot·后端
why技术7 小时前
Stack Overflow,轰然倒下!
前端·人工智能·后端
幽络源小助理7 小时前
SpringBoot基于Mysql的商业辅助决策系统设计与实现
java·vue.js·spring boot·后端·mysql·spring
ai小鬼头8 小时前
AIStarter如何助力用户与创作者?Stable Diffusion一键管理教程!
后端·架构·github
简佐义的博客8 小时前
破解非模式物种GO/KEGG注释难题
开发语言·数据库·后端·oracle·golang
Code blocks9 小时前
使用Jenkins完成springboot项目快速更新
java·运维·spring boot·后端·jenkins
荔枝吻9 小时前
【沉浸式解决问题】idea开发中mapper类中突然找不到对应实体类
java·intellij-idea·mybatis