专栏名称: 芋道源码
纯 Java 源码分享公众号,目前有「Dubbo」「SpringCloud」「Java 并发」「RocketMQ」「Sharding-JDBC」「MyCAT」「Elastic-Job」「SkyWalking」「Spring」等等
目录
相关文章推荐
沉默王二  ·  12月跳槽的兄弟注意了。。 ·  5 天前  
芋道源码  ·  高级进阶:复杂业务系统的通用架构设计 ·  5 天前  
芋道源码  ·  后端行情变了,差别真的挺大! ·  5 天前  
51好读  ›  专栏  ›  芋道源码

详解企业级数据脱敏方案

芋道源码  · 公众号  · Java  · 2024-12-13 09:30

正文

👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入芋道快速开发平台知识星球。下面是星球提供的部分资料: 

👉这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号、CRM 等等功能:

  • Boot 仓库:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 仓库:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn
【国内首批】支持 JDK 21 + SpringBoot 3.2.2、JDK 8 + Spring Boot 2.7.18 双版本 

来源:juejin.cn/post/
7367577126855852066


简介

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

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

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

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

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

企业脱敏方案

企业如何实现脱敏?我们先来看典型的系统数据交互链路,数据需要经过数据库、后端应用、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执行流程

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

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

本文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;
}    

2.实现自定义拦截。

/***
** 加密拦截
***/

@Intercepts({@Signature(
        type = Executor.class,
        method 
"update",
        args = {MappedStatement.classObject.class}
), @Signature(
        type 
= Executor.class,
        method 
"query",
        args = {MappedStatement.classObject.classRowBounds.classResultHandler.classCacheKey.classBoundSql.class}
), @Signature(
        type 
= Executor.class,
        method 
"query",
        args = {MappedStatement.classObject.classRowBounds.classResultHandler.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修改,完成加解密逻辑。

public class PluginService {

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

    private static final Map> 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                 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 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 EncryptTagFields) throws Throwable {
        if (object != null && !EncryptTagFields.isEmpty()) {
            String[] originalValues = new String[EncryptTagFields.size()];

            for(int i = 0; i                 Field field = (Field)EncryptTagFields.get(i);
                String value = (String)field.get(object);
                originalValues[i] = value;
            }

            for(int i = 0; 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 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 encryptTagFields) throws Throwable {
        if (encryptTagFields.isEmpty()) {
            return;
        }

        for (int i = 0; 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 findEncryptTagFields(Object object) {
        Class clazz = object.getClass();

        List fieldList = new ArrayList<>();
        for(; clazz != null; clazz = clazz.getSuperclass()) {
            Field[] declaredFields = clazz.getDeclaredFields();
            int length = declaredFields.length;

            for(int index = 0; 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                 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 序列化时在最后添加内容

通过实现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","[email protected]"),new FastjsonValueFilter());

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

输出脱敏

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

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

@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 mediaTypes = new ArrayList<>();
        //设定json格式且编码为UTF-8
        mediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
        fastConverter.setSupportedMediaTypes(mediaTypes);
        
        //添加全局自定义脱敏
        fastJsonConfig.setSerializeFilters(new ValueDesensitizeFilter());
       //添加指定类脱敏方法
        Map, SerializeFilter> classSerializeFilters = new HashMap<>();
        classSerializeFilters.put(Employee.classnew FastjsonValueFilter());
        fastJsonConfig.setClassSerializeFilters(classSerializeFilters);

        
        // 3.在converter中添加配置信息
        fastConverter.setFastJsonConfig(fastJsonConfig);
        // 4.将converter赋值给HttpMessageConverter
        HttpMessageConverter> converter = fastConverter;
        // 5.返回HttpMessageConverters对象
        return new HttpMessageConverters(converter);
    }
}

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

总结

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


欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,长按”或“扫描”下方二维码噢

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

文章有帮助的话,在看,转发吧。

谢谢支持哟 (*^__^*)