简介 企业脱敏方案 数据库脱敏方案 日志脱敏方案 输出脱敏 总结
简介
最近几年经常发生用户数据泄漏的事件,给企业带来危机。随着用户对个人隐私数据的重视和法律法规的完善,数据安全显得愈发重要。一方面可以加强权限管理,减少能够接触数据的人员以及导出数据加强审批。另一方面,还需要从技术上对用户隐私数据进行脱敏处理,提高数据的安全性。
数据脱敏方法有很多种,大致可以按照以下进行分类:
隐藏法: 只显示敏感信息的部分内容,其他部分进行遮挡,比较常见使用星号替代。这种方式日常比较多见,比如手机号,银行卡号等只显示后面和后面几位,好处是虽然只是部分内容显示,但足够提供有效信息,同时不会暴露完整数据。 混淆法: 对原有数据截断、替换、隐藏、数字进行随机移位,使得原有数据完全失真或者部分失真,混淆真假。 加密: 通过加密密钥和算法对敏感数据进行加密得到密文,密文可见但是完全没有可读意义,是脱敏最彻底的方法。其中对称加密还能密钥解密可以从密文恢复原始数据。比如密码保存采用非对称加密,手机号存储时采用对称加密。
用户的敏感数据包含姓名、电话号码、身份证、银行卡号、电子邮件、家庭住址、登录密码等等。需要考虑数据的敏感程度、数据安全要求以及实际业务使用场景选择合适的脱敏方法。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里面添加脱敏注解标记。
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<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 序列化时在最后添加内容
通过实现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());
在标记脱敏字段以及对应方法时,可以通过配置的方法, 对类相关的脱敏字段以及方法进行封装。要求不高的话添加响应的注解也可实现。
输出脱敏
在输出层织入切面进行拦截,在切面内实现脱敏逻辑。实现逻辑跟日志脱敏类似,需要对脱敏字段进行标记以及对应脱敏方法。
如果是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<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);
}
}
总结
本文总结了企业中脱敏方案实现,包含数据库脱敏、日志脱敏、输出脱敏,并贴上关键实现代码。能够满足业务的要求。
如喜欢本文,请点击右上角,把文章分享到朋友圈
如有想了解学习的技术点,请留言给若飞安排分享
因公众号更改推送规则,请点“在看”并加“星标”第一时间获取精彩技术分享
·END·
相关阅读:
一张图看懂微服务架构路线 基于Spring Cloud的微服务架构分析 微服务等于Spring Cloud?了解微服务架构和框架 如何构建基于 DDD 领域驱动的微服务? 微服务架构实施原理详解 微服务的简介和技术栈 微服务场景下的数据一致性解决方案 设计一个容错的微服务架构
作者:扎Zn了老Fe
来源:juejin.cn/post/7367577126855852066
版权申明:内容来源网络,仅供学习研究,版权归原创者所有。如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!
我们都是架构师!
关注架构师(JiaGouX),添加“星标”
获取每天技术干货,一起成为牛逼架构师
技术群请加若飞:1321113940 进架构师群
投稿、合作、版权等邮箱:admin@137x.com