基于AOP切面的数据脱敏

admin 2023年12月22日19:38:46评论35 views字数 9649阅读32分9秒阅读模式

随着数据安全法和个人信息保护法的实施,我们都知道,保护数据是企业信息化建设中非常重要的事情。

数据的生命周期包括:采集、传输、存储、处理、交换、销毁6个阶段

DSMM定义了数据生命周期中每个阶段需要做的一些安全控制,比如采集阶段进行数据分级,传输阶段采用加密传输协议,存储阶段使用强加密算法对敏感数据进行加密存储,处理阶段实施脱敏以及访问控制,交换阶段使用隐私计算,销毁阶段使用净化技术使数据无法被任何技术手段恢复等。

当然每个阶段不仅仅只有上面描诉的安全控制,以及并不是哪个阶段必须要使用这些安全控制。比如不同组织之间的数据交换过程,并非要使用隐私计算,可以通过其他的安全策略达到组织想要的安全目的,可以是技术性控制也可以是管理性控制/行政控制。

本文的重点是如何在微服务架构中通过AOP切面编程实现敏感数据脱敏,针对其他的内容不过多阐述,比如访问控制、加解密的技术实现都可以单独的写一篇文章。

当前微服务架构,一个系统拆分成很多的模块进行开发。每个模块的开发人员只需要写和业务相关的代码即可,比如订单模块、优惠券模块、商品模块、资产模块都可以看作是单独的应用。各个模块之间功能使用HTTP或RPC协议进行调用,各个微服务应用之间可以按需组合。比如订单模块和优惠券模块组合成一个链路,比如优惠券、资产、订单三个应用组合成一个链路,链路的入口是API网关。

数据安全发展至今,脱敏技术分为“静态脱敏”和“动态脱敏“两种。静态脱敏由于改变了元数据,对业务影响很大,所以现在市面上大部分脱敏产品使用的是动态脱敏技术,即边脱敏边使用,并不会修改原有数据内容。

由于业务系统越来越复杂,一个系统中的多个app对数据的访问需求是不一样的。比如订单应用需要查看用户手机号明文,而资产应用需要看到用户脱敏的手机号。所以,

基于AOP切面的数据脱敏

脱敏可以在单个应用中实现,也可以在Api gateway中实现,也可以在数据库实现,但每种实现方式都有自己的优缺点。

在生产环境中,一般有2种选择,单个应用的逻辑中进行脱敏或在Api网关中实现脱敏逻辑。

基于AOP切面的数据脱敏

应用自身进行脱敏,优点是可以根据业务的数据访问需求,自定义自己的数据访问控制和脱敏方案。缺点是写业务的程序员在写业务逻辑之外增加了额外的开发成本。

另外一种方式是网关层脱敏

基于AOP切面的数据脱敏

网关层脱敏的优点是,无需侵入业务代码逻辑,不会增加单个应用的开发成本。而缺点是,无法根据每个应用对数据的安全需求进行权限控制和脱敏方式选择。网关层并不知道每个应用中比如app1、app2中需要脱敏的实体类中的对象,比如app1中的Person中的name对象、phone对象,app2中的address对象,所以在网关层脱敏是比较难实现的一件事。

我在两家甲方待过,不论哪一家,我的脱敏方案都是业务层+动态脱敏。

我封装好一个脱敏工具包,包含对姓名,手机号,身份证,电子邮箱,地址,银行卡号进行脱敏。

1、先定义个需要脱敏的枚举实体类

@Getter@AllArgsConstructorpublic enum ReadableSensitiveTypeEnum {
/** * 身份证编号 */ ID_CARD("身份证"),
/** * 地址/住址 */ ADDRESS("地址"),
/** * 姓名 */ NAME("姓名"),
/** * 手机号 */ PHONE("手机号"),
/** * 手机号 */ EMAIL("邮箱"),
/** * 银行卡号 */ BANK_CARD_NO("银行卡号");
private String desc;
}

2、定义一个需要脱敏的注解

@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.FIELD})public @interface ReadableSensitiveVerify {
ReadableSensitiveTypeEnum value();
}

3、然后再编写一个脱敏工具类


public class DesensitizationUtils {
/** * @description: 名字脱敏     * 脱敏规则: 隐藏中中间部分,比如:李某人 置换为 李*人 , 李某置换为 *某,司徒司翘置换为 司**翘 */ public static String desensitizedName(String fullName){ if (!Strings.isNullOrEmpty(fullName)) { int length = fullName.length(); if(length == 2){ return "*".concat(fullName.substring(1)); }else if(length == 3){ return StringUtils.left(fullName,1).concat("*").concat(StringUtils.right(fullName,1)); }else if(length > 3){ return StringUtils.left(fullName,1).concat(generateAsterisk(fullName.substring(1,length-1).length())).concat(StringUtils.right(fullName,1)); }else { return fullName; } } return fullName; }

/**    * @description: 手机号脱敏,脱敏规则: 保留前三后四, 比如15566026528置换为155****6528 */ public static String desensitizedPhoneNumber(String phoneNumber){ if(StringUtils.isNotEmpty(phoneNumber)){ int length = phoneNumber.length(); if(length == 11){ return phoneNumber.replaceAll("(\w{3})\w*(\w{4})", "$1****$2"); }else if(length > 2){ return StringUtils.left(phoneNumber,1).concat(generateAsterisk(phoneNumber.substring(1,length-2).length())).concat(StringUtils.right(phoneNumber,1)); }else { return phoneNumber; } } return phoneNumber; }

/** * @description: 身份证脱敏 * 脱敏规则: 保留前六后三, 适用于15位和18位身份证号: * 原身份证号(15位):210122198401187,脱敏后的身份证号:210122******187     * 原身份证号(18位):210122198401187672,脱敏后的身份证号:210122*********672 */ public static String desensitizedIdNumber(String idNumber){ if (!Strings.isNullOrEmpty(idNumber)) { int length = idNumber.length(); if (length == 15){ return idNumber.replaceAll("(\w{6})\w*(\w{3})", "$1******$2"); }else if (length == 18){ return idNumber.replaceAll("(\w{6})\w*(\w{3})", "$1*********$2"); }else if(length > 9){ return StringUtils.left(idNumber,6).concat(generateAsterisk(idNumber.substring(6,length-3).length())).concat(StringUtils.right(idNumber,3)); } } return idNumber; }

/**    * @description: 电子邮箱脱敏,脱敏规则:电子邮箱隐藏@前面的3个字符 */ public static String desensitizationEmail(String email) { if (StringUtils.isEmpty(email)) { return email; } String encrypt = email.replaceAll("(\w+)\w{3}@(\w+)", "$1***@$2"); if (email.equalsIgnoreCase(encrypt)) { encrypt = email.replaceAll("(\w*)\w{1}@(\w+)", "$1*@$2"); } return encrypt; }

/**    * @description: 地址脱敏,脱敏规则:从第4位开始隐藏,隐藏8位 */ public static String desensitizedAddress(String address){ if (!Strings.isNullOrEmpty(address)) { int length = address.length(); if(length > 4 && length <= 12){ return StringUtils.left(address, 3).concat(generateAsterisk(address.substring(3).length())); }else if(length > 12){ return StringUtils.left(address,3).concat("********").concat(address.substring(11)); }else { return address; } } return address; }

/**    * @description: 银行账号脱敏, 脱敏规则:银行账号保留前六后四 */ public static String desensitizedAddressBankCardNum(String acctNo) { if (StringUtils.isNotEmpty(acctNo)) { String regex = "(\w{6})(.*)(\w{4})"; Matcher m = Pattern.compile(regex).matcher(acctNo); if (m.find()) { String rep = m.group(2); StringBuilder sb = new StringBuilder(); for (int i = 0; i < rep.length(); i++) { sb.append("*"); } acctNo = acctNo.replaceAll(rep, sb.toString()); } } return acctNo; }

/**    * @description: 返回指定长度*字符串 */ private static String generateAsterisk(int length){ String result = ""; for (int i = 0; i < length; i++) { result += "*"; } return result; }}


然后在app1、和app2中只需要导入脱敏包,对实体类中需要进行脱敏的字段添加注解。

@Data@EqualsAndHashCode()@ApiModel(value="CustomerInfoListVo对象", description="个人客户列表信息")public class CustomerInfoListVo implements Serializable {
@ApiModelProperty(value = "记录id") private Integer id;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8") @ApiModelProperty(value = "创建时间") private Date createTime;
@ApiModelProperty(value = "客户名称") @ReadableSensitiveVerify(ReadableSensitiveTypeEnum.NAME) private String realName;
@ApiModelProperty(value = "手机号码") @ReadableSensitiveVerify(ReadableSensitiveTypeEnum.PHONE) private String phone;
@ApiModelProperty(value = "客户身份证号") @ReadableSensitiveVerify(ReadableSensitiveTypeEnum.ID_CARD) private String idCarNumber;
@ApiModelProperty(value = "性别") private String gender;
@ApiModelProperty(value = "出生日期") @JsonFormat(pattern = "yyyy-MM-dd",timezone="GMT+8") private Date birthDate;
@ApiModelProperty(value = "客户编号") private String customerCode;
@ApiModelProperty(value = "邮箱") private String email;
@ApiModelProperty(value = "客户经理") private String customerManager;
@ApiModelProperty(value = "微信号") private String weChat;
@ApiModelProperty(value = "拉黑时间") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8") private Date blockingTime;
@ApiModelProperty(value = "拉黑说明") private String blockInstructions;
@ApiModelProperty(value = "意向等级") private String interestingGrade;}

最后在业务代码中编写一个切面类


/**@description: 返回值数据脱敏处理aop*/@Slf4j@Component@Aspectpublic class DesensitizationAspect {
@Autowired private UserUtils userUtils;
/** * @description: 切入点 * @return: * @author: Ming * @time: 2022/6/22 */ @Pointcut("execution(* com.yptx.financialsystem.*.controller.*.*(..))") public void pointCut() { }

/** * @description: 返回值处理 * @return: * @author: Ming * @time: 2022/6/22 */ @Around("pointCut()") public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { SysUser user = userUtils.getSysUser(); log.info("user: {}",user); //typeEnums表示用户拥有哪些字段的查看权限,没有的就脱敏处理 List<ReadableSensitiveTypeEnum> typeEnums = new ArrayList<>(); if(user != null){ typeEnums = user.getReadableSensitives() == null ? typeEnums : user.getReadableSensitives(); } log.info("typeEnums: {}",typeEnums); Object obj = proceedingJoinPoint.proceed(); if (obj == null || isPrimitive(obj.getClass())) { return obj; } dealData(obj,typeEnums); return obj; }
/** * @description: 基本数据类型和String类型判断 * @return: * @author: Ming * @time: 2022/6/23 */ private boolean isPrimitive(Class<?> clz) { try { if (String.class.isAssignableFrom(clz) || clz.isPrimitive()) { return true; } else { return ((Class) clz.getField("TYPE").get(null)).isPrimitive(); } } catch (Exception e) { return false; } }

/** * @description: 数据处理 * @return: * @author: Ming * @time: 2022/6/23 */ private void dealData(Object obj,List<ReadableSensitiveTypeEnum> typeEnums){ if (null == obj) {return;} if (obj.getClass().isPrimitive()) {return;}// 是否是接口 if (obj.getClass().isInterface()) {return;} Object data = ((ResponseJson) obj).getObj(); if(data != null){ Class<?> clazz = data.getClass(); if (clazz.equals(Page.class)) { Page page = (Page) data; List<?> record = page.getRecords(); for (Object o : record) { Field[] fields = o.getClass().getDeclaredFields(); replace(fields,o,typeEnums); } }else { Field[] fields = clazz.getDeclaredFields(); replace(fields,data,typeEnums); } } }

/** * @description: 脱敏敏感字段 * @return: * @author: Ming * @time: 2022/6/23 */ private void replace(Field[] fields,Object o,List<ReadableSensitiveTypeEnum> typeEnums){ try { for (Field f : fields) { if(f != null){ //设置private字段可访问 f.setAccessible(true); //处理自定义vo作为属性(属性类型非自身类对象类型的属性) CustomEntityDesensitizationVerify custom = f.getAnnotation(CustomEntityDesensitizationVerify.class); if(custom != null){ Object customEntity = f.get(o); Field[] entityFiled = customEntity.getClass().getDeclaredFields(); replace(entityFiled, customEntity,typeEnums); } //处理list属性 Class<?> curFieldType = f.getType(); if (curFieldType.equals(List.class)) { List<?> record = (List<?>) f.get(o); if(record != null && !record.isEmpty() ){ for (Object obj :record) { Field[] ff= obj.getClass().getDeclaredFields(); replace(ff,obj,typeEnums); } } } //处理普通字符串字段 ReadableSensitiveVerify annotation = f.getAnnotation(ReadableSensitiveVerify.class); if(annotation != null){ f.getType(); String valueStr = (String) f.get(o); if(StringUtils.isNotEmpty(valueStr)){ ReadableSensitiveTypeEnum type = annotation.value(); if(type.equals(ReadableSensitiveTypeEnum.NAME) && !typeEnums.contains(type)){ f.set(o, DesensitizationUtils.desensitizedName(valueStr)); } if(type.equals(ReadableSensitiveTypeEnum.ID_CARD) && !typeEnums.contains(type) ){ f.set(o, DesensitizationUtils.desensitizedIdNumber(valueStr)); } if(type.equals(ReadableSensitiveTypeEnum.ADDRESS) && !typeEnums.contains(type)){ f.set(o, DesensitizationUtils.desensitizedAddress(valueStr)); } if(type.equals(ReadableSensitiveTypeEnum.PHONE) && !typeEnums.contains(type)){ f.set(o, DesensitizationUtils.desensitizedPhoneNumber(valueStr)); } if(type.equals(ReadableSensitiveTypeEnum.BANK_CARD_NO) && !typeEnums.contains(type)){ f.set(o, DesensitizationUtils.desensitizedAddressBankCardNum(valueStr)); } if(type.equals(ReadableSensitiveTypeEnum.EMAIL) && !typeEnums.contains(type)){ f.set(o, DesensitizationUtils.desensitizationEmail(valueStr)); } } } } } }catch (Exception e){ e.printStackTrace(); } }}


具体的效果

基于AOP切面的数据脱敏

总结:业务要实现数据安全的要求,不是一个脱敏就能解决问题的,动态脱敏技术只是在数据处理过程中返回给的用户的数据是脱敏的,但存储在数据库中的静态数据仍然是明文的,从威胁建模的角度而言,仍然存在敏感数据泄漏的风险,所以对数据分级的最高密级数据要做加密存储是必要的,并且需要通过某些安全措施保证加解密的密钥不会泄漏。

原文始发于微信公众号(信息安全笔记):基于AOP切面的数据脱敏

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年12月22日19:38:46
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   基于AOP切面的数据脱敏http://cn-sec.com/archives/2329073.html

发表评论

匿名网友 填写信息