最近几年经常发生用户数据泄漏的事件,给企业带来危机。随着用户对个人隐私数据的重视和法律法规的完善,数据安全显得愈发重要。一方面可以加强权限管理,减少能够接触数据的人员以及导出数据加强审批。另一方面,还需要从技术上对用户隐私数据进行脱敏处理,提高数据的安全性。
数据脱敏方法有很多种,大致可以按照以下进行分类:
隐藏法: 只显示敏感信息的部分内容,其他部分进行遮挡,比较常见使用星号替代。这种方式日常比较多见,比如手机号,银行卡号等只显示后面和后面几位,好处是虽然只是部分内容显示,但足够提供有效信息,同时不会暴露完整数据。
混淆法: 对原有数据截断、替换、隐藏、数字进行随机移位,使得原有数据完全失真或者部分失真,混淆真假。
加密: 通过加密密钥和算法对敏感数据进行加密得到密文,密文可见但是完全没有可读意义,是脱敏最彻底的方法。其中对称加密还能密钥解密可以从密文恢复原始数据。比如密码保存采用非对称加密,手机号存储时采用对称加密。
用户的敏感数据包含姓名、电话号码、身份证、银行卡号、电子邮件、家庭住址、登录密码等等。需要考虑数据的敏感程度、数据安全要求以及实际业务使用场景选择合适的脱敏方法。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里面添加脱敏注解标记。
publicclass Employee { private Long id;private String name;@EncryptTagprivate String mobile;private String mobileEncrypt;private String email;privatedoublesalary;}
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})})publicclass EncryptPlugin implements Interceptor {@OverridepublicObject intercept(Invocation invocation)throws Throwable { MappedStatement mappedStatement=(MappedStatement)invocation.getArgs()[0];Object param=invocation.getArgs()[1];PluginService.encrypt(invocation,param);returninvocation.proceed();}@OverridepublicObject plugin(Object target){returnPlugin.wrap(target,this);}@Overridepublicvoid setProperties(Properties properties){ } }/*** ** 解密拦截 ***/@Intercepts({@Signature(type=ResultSetHandler.class,method="handleResultSets",args={Statement.class})})publicclass DecryptPlugin implements Interceptor {@OverridepublicObject intercept(Invocation invocation)throws Throwable { Object result=invocation.proceed();if(result!=null&&result instanceof List){ this.decrypt(((List)result).iterator());}returnresult;}@OverridepublicObject plugin(Object target){returnPlugin.wrap(target,this);}@Overridepublicvoid setProperties(Properties properties){ } private void decrypt(Iterator iterator)throws Throwable {while(iterator.hasNext()){ Object object=iterator.next();PluginService.decrypt(object);} } }
3.实现sql修改,完成加解密逻辑。
publicclass PluginService { privatestaticfinal Logger LOGGER=LoggerFactory.getLogger(PluginService.class);privatestaticfinal Map<String,List<Field>>ENCRYPT_TAG_FIELDS=new ConcurrentHashMap();publicstatic void encrypt(Invocation invocation,Object object)throws Throwable {if(object.getClass().isArray()){intlength=Array.getLength(object);if(length<=0){return;}for(inti=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(inti=0;i<EncryptTagFields.size();++i){ Field field=(Field)EncryptTagFields.get(i);Stringvalue=(String)field.get(object);originalValues[i]=value;}for(inti=0;i<EncryptTagFields.size();++i){ Field field=(Field)EncryptTagFields.get(i);Stringvalue=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(Stringvalue){ String encryptValue=value+"encrypt";returnencryptValue;}publicstatic 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(inti=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){ Stringvalue=(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();intlength=declaredFields.length;for(intindex=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());} } } }returnfieldList;} 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);returnencyptField;} }publicstatic Field getField(Object object,String fieldName){for(Class clazz=object.getClass();clazz!=null;clazz=clazz.getSuperclass()){ Field[]var3=clazz.getDeclaredFields();intvar4=var3.length;for(intvar5=0;var5<var4;++var5){ Field field=var3[var5];if(field.getName().equals(fieldName)){returnfield;} } } returnnull;} }
日志脱敏,核心在于序列化时对于敏感字段修改其序列化方式。各大序列化工具一般都有序列化自定义功能,关注公众号:码猿技术专栏,回复关键词:IDEA 获取最新版IDEA破解脚本!本文以fastjson为例讲解实现,实现方式有两种:
基于注解@JSONField实现
基于序列化过滤器
@JSONField方式不建议使用,对业务入侵太大。另外一种继续序列化过滤器,fastjson提供了多种SerializeFilter
:
PropertyPreFilter 根据PropertyName判断是否序列化
PropertyFilter 根据PropertyName和PropertyValue来判断是否序列化
NameFilter 修改Key,如果需要修改Key,process返回值则可
ValueFilter 修改Value
BeforeFilter 序列化时在最前添加内容
AfterFilter 序列化时在最后添加内容
通过实现ValueFilter自定义序列化扩展,针对目标类以及字段进行脱敏返回。
核心代码简化如下:
publicclass FastjsonValueFilter implements ValueFilter {@OverridepublicObject process(Object object,String name,Objectvalue){if(needDesensitize(object,name)){returndesensitize(value);} } } String s=JSON.toJSONString(new Person("131xxxx1552","123@163.com"),new FastjsonValueFilter());
在标记脱敏字段以及对应方法时,可以通过配置的方法, 对类相关的脱敏字段以及方法进行封装。要求不高的话添加响应的注解也可实现。
在输出层织入切面进行拦截,在切面内实现脱敏逻辑。实现逻辑跟日志脱敏类似,需要对脱敏字段进行标记以及对应脱敏方法。
如果是Spring Boot集成,配置 Spring MVC 的话只需继承 WebMvcConfigurer
覆写 configureMessageConverters
方法,支持全局和指定类脱敏配置,示例如下:
@Configurationpublicclass FastJsonWebSerializationConfiguration implements WebMvcConfigurer {@Bean(name="httpMessageConverters")publicHttpMessageConverters 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-8mediaTypes.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赋值给HttpMessageConverterHttpMessageConverter<?>converter=fastConverter;// 5.返回HttpMessageConverters对象returnnew HttpMessageConverters(converter);} }
本文总结了企业中脱敏方案实现,包含数据库脱敏、日志脱敏、输出脱敏,并贴上关键实现代码。能够满足业务的要求。