最近几年经常发生用户数据泄漏的事件,给企业带来危机。随着用户对个人隐私数据的重视和法律法规的完善,数据安全显得愈发重要。一方面可以加强权限管理,减少能够接触数据的人员以及导出数据加强审批。另一方面,还需要从技术上对用户隐私数据进行脱敏处理,提高数据的安全性。
数据脱敏方法有很多种,大致可以按照以下进行分类:
隐藏法: 只显示敏感信息的部分内容,其他部分进行遮挡,比较常见使用星号替代。这种方式日常比较多见,比如手机号,银行卡号等只显示后面和后面几位,好处是虽然只是部分内容显示,但足够提供有效信息,同时不会暴露完整数据。混淆法: 对原有数据截断、替换、隐藏、数字进行随机移位,使得原有数据完全失真或者部分失真,混淆真假。加密: 通过加密密钥和算法对敏感数据进行加密得到密文,密文可见但是完全没有可读意义,是脱敏最彻底的方法。其中对称加密还能密钥解密可以从密文恢复原始数据。比如密码保存采用非对称加密,手机号存储时采用对称加密。用户的敏感数据包含姓名、电话号码、身份证、银行卡号、电子邮件、家庭住址、登录密码等等。需要考虑数据的敏感程度、数据安全要求以及实际业务使用场景选择合适的脱敏方法。Hutool包里面提供了许多常用的脱敏方法。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/ruoyi-vue-pro 视频教程:https://doc.iocoder.cn/video/ 企业如何实现脱敏?我们先来看典型的系统数据交互链路,数据需要经过数据库、后端应用、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.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修改,完成加解密逻辑。
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方式不建议使用,对业务入侵太大。另外一种继续序列化过滤器,fastjson提供了多种SerializeFilter
:
PropertyPreFilter 根据PropertyName判断是否序列化PropertyFilter 根据PropertyName和PropertyValue来判断是否序列化NameFilter 修改Key,如果需要修改Key,process返回值则可通过实现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.class , new 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/ 本文总结了企业中脱敏方案实现,包含数据库脱敏、日志脱敏、输出脱敏,并贴上关键实现代码。能够满足业务的要求。