最近几年经常发生用户数据泄漏的企业事件,给企业带来危机。数据脱随着用户对个人隐私数据的敏方重视和法律法规的完善,数据安全显得愈发重要。企业一方面可以加强权限管理,数据脱减少能够接触数据的敏方人员以及导出数据加强审批。另一方面,企业还需要从技术上对用户隐私数据进行脱敏处理,数据脱提高数据的敏方安全性。 数据脱敏方法有很多种,企业大致可以按照以下进行分类: 隐藏法: 只显示敏感信息的数据脱部分内容,其他部分进行遮挡,敏方比较常见使用星号替代。企业这种方式日常比较多见,数据脱比如手机号,敏方银行卡号等只显示后面和后面几位,好处是虽然只是部分内容显示,但足够提供有效信息,同时不会暴露完整数据。混淆法: 对原有数据截断、替换、隐藏、数字进行随机移位,使得原有数据完全失真或者部分失真,混淆真假。加密: 通过加密密钥和算法对敏感数据进行加密得到密文,亿华云计算密文可见但是完全没有可读意义,是脱敏最彻底的方法。其中对称加密还能密钥解密可以从密文恢复原始数据。比如密码保存采用非对称加密,手机号存储时采用对称加密。        用户的敏感数据包含姓名、电话号码、身份证、银行卡号、电子邮件、家庭住址、登录密码等等。需要考虑数据的敏感程度、数据安全要求以及实际业务使用场景选择合适的脱敏方法。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.                                                                总结本文总结了企业中脱敏方案实现,包含数据库脱敏、日志脱敏、输出脱敏,并贴上关键实现代码。能够满足业务的要求。  |