基于注解的字段脱敏,无侵入代码
好久没更新了,最近一直忙于加班。
之前脱敏的规则,抽象出来做成公共方法。方便其他业务引用。
1 之前的做法
有前端传一个字段,是否需要脱敏,后端根据业务字段进行脱敏,这样的脱敏是放在业务中。
这样做就对代码有污染,有更新的字段就得继续加代码。而且别的业务需要,也得自己添加,既不简洁,又显冗余。
尤其是在微服务阶段,底层服务本身不关心脱敏业务,不需要坐这个业务;中台业务每个业务都得自己写,显得十分冗余。
2 基于注解的脱敏方式
业务的脱敏规则,基本大差不差。例如手机号屏蔽中间四位,姓名只保留姓等。所以这些业务完全可以基于注解来解决。
如果有特殊的规则,在注解上添加规则即可。
这里会用到 JsonPath 的语法,用之前,请点击连接了解。
JsonPath提供的json解析非常强大,它提供了类似正则表达式的语法,基本上可以满足所有你想要获得的json内容。
引用fastjson ,提供了jsonpath的方法。
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.83</version> <scope>compile</scope> </dependency>
这里脱敏规则有两种实现方式,可以打在方法上,也可以打在字段上。
因为这边前端需要手动打开与展示,并且通过了额外的字段来控制,这边是用的打在类上,没有使用在方法上。
当然也可以做到在字段上进行脱敏,这边又是一种实现方式。
这样做也有风险,需要脱敏字段的和识别脱敏字段的改变,如果同步更新到注解上,会造成字段映射不到,注解失效。
注解:
这里根据自己业务进行定义
package com.acxiom.crm5.common.core.desensitization;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveWrapped {
/**
* jsonPath路径[n],用于获取接口入参的一个bool值,标识接口是否需要脱敏
* @return
*/
String flagKey() default "";
/**
* 需要脱敏得字段
* @return
*/
Field[] field();
@interface Field {
/**
* jsonPath路径$.records[*].cstFullName,用于获取脱敏字段
* @return
*/
String flagKey() default "";
/**
* jsonPath路径[n].showName,用于获取一个bool值标识当前字段是否需要脱敏
* @return
*/
String key();
/**
* SensitiveEnum标识字段的脱敏类型
*
* @return
*/
SensitiveEnum type();
}
}
实现:
package com.acxiom.crm5.common.core.desensitization; import com.alibaba.fastjson.JSONPath; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import java.util.Collection; import java.util.List; @Aspect @Component public class SensitiveHandler { @Around("@annotation(annotation)") public Object desensitizationAspect(ProceedingJoinPoint joinPoint, SensitiveWrapped annotation) throws Throwable { Signature signature = joinPoint.getSignature(); joinPoint.getTarget().getClass().getDeclaredMethods(); Object[] args = joinPoint.getArgs(); Object result = joinPoint.proceed(); if (result != null) { desensitization(result, args, annotation); } return result; } public void desensitization(Object result, Object[] args, SensitiveWrapped annotation) { //所有数据脱敏 所有字段加密 boolean needToDo = "".equals(annotation.flagKey()); if (!needToDo) { needToDo = (boolean) JSONPath.eval(args, annotation.flagKey()); } if (!needToDo) { return; } // 指定字段脱敏 flagKey()为false的加密,true 明文 for (SensitiveWrapped.Field field : annotation.field()) { boolean doIt = "".equals(field.flagKey()); if (!doIt) { doIt = (boolean) JSONPath.eval(args, field.flagKey()); } if (!doIt) { continue; } swap(result, field); } } /** * 字段替换 敏感字段 --> * * * @param result * @param field */ private void swap(Object result, SensitiveWrapped.Field field) { String key = field.key(); SensitiveEnum type = field.type(); if (key.contains("*")) { String prefix = key.substring(0, key.indexOf("*") - 1); String suffix = key.substring(key.indexOf("*") + 2); List<String> sourceValue = (List<String>) JSONPath.eval(result, key); Collection collection = (Collection) JSONPath.eval(result, prefix); for (int i = 0, j = 0; i < collection.size(); i++) { String path = prefix + "[" + i + "]" + suffix; if (JSONPath.eval(result, path) != null) { JSONPath.set(result, path, getDesensitizationValue(type, sourceValue.get(j++))); } } } else { String sourceValue = (String) JSONPath.eval(result, key); if (StringUtils.isNoneBlank(sourceValue)) { String newValue = getDesensitizationValue(type, sourceValue); JSONPath.set(result, key, newValue); } } } public String getDesensitizationValue(SensitiveEnum type, String value) { switch (type) { case CHINESE_NAME: { value = SensitiveInfoUtils.chineseName(value); break; } case LAST_NAME: { value = SensitiveInfoUtils.lastName(value); break; } case FIRST_NAME: { value = SensitiveInfoUtils.firstName(value); break; } case ID_CARD: { value = SensitiveInfoUtils.idCardNum(value); break; } case FIXED_PHONE: { value = SensitiveInfoUtils.fixedPhone(value); break; } case MOBILE_PHONE: { value = SensitiveInfoUtils.mobilePhone(value); break; } case ADDRESS: { value = SensitiveInfoUtils.address(value, 4); break; } case EMAIL: { value = SensitiveInfoUtils.email(value); break; } case BANK_CARD: { value = SensitiveInfoUtils.bankCard(value); break; } case CNAPS_CODE: { value = SensitiveInfoUtils.cnapsCode(value); break; } case SEX: { value = SensitiveInfoUtils.sex(value); break; } case BIRTHDAY: { value = SensitiveInfoUtils.birthDay(value); break; } } return value; } }
工具类:
package com.acxiom.crm5.common.core.desensitization; import org.apache.commons.lang3.StringUtils; public class SensitiveInfoUtils { /** * [中文姓名] 只显示第一个汉字,其他隐藏为2个星号<例子:李**> */ public static String chineseName(final String fullName) { if (StringUtils.isBlank(fullName)) { return ""; } final String name = StringUtils.left(fullName, 1); return StringUtils.rightPad(name, StringUtils.length(fullName), "*"); } /** * [中文姓名] 只显示第一个汉字,其他隐藏为2个星号<例子:李**> */ public static String lastName(final String lastName) { if (StringUtils.isBlank(lastName)) { return ""; } final String name = StringUtils.left(lastName, 1); return StringUtils.rightPad(name, StringUtils.length(lastName), "*"); } /** * [中文姓名] 只显示第一个汉字,其他隐藏为2个星号<例子:李**> */ public static String firstName(final String firstName) { if (StringUtils.isBlank(firstName)) { return ""; } final String name = StringUtils.left(firstName, 1); return StringUtils.rightPad(name, StringUtils.length(firstName), "*"); } /** * [中文姓名] 只显示第一个汉字,其他隐藏为2个星号<例子:李**> */ public static String chineseName(final String familyName, final String givenName) { if (StringUtils.isBlank(familyName) || StringUtils.isBlank(givenName)) { return ""; } return chineseName(familyName + givenName); } /** * [身份证号] 显示最后四位,其他隐藏。共计18位或者15位。<例子:420**********5762> */ public static String idCardNum(final String id) { if (StringUtils.isBlank(id)) { return ""; } return StringUtils.left(id, 3).concat(StringUtils .removeStart(StringUtils.leftPad(StringUtils.right(id, 4), StringUtils.length(id), "*"), "***")); } /** * [固定电话] 后四位,其他隐藏<例子:****1234> */ public static String fixedPhone(final String num) { if (StringUtils.isBlank(num)) { return ""; } return StringUtils.leftPad(StringUtils.right(num, 4), StringUtils.length(num), "*"); } /** * [手机号码] 前三位,后四位,其他隐藏<例子:138******1234> */ public static String mobilePhone(final String num) { if (StringUtils.isBlank(num)) { return ""; } return StringUtils.left(num, 3).concat(StringUtils .removeStart(StringUtils.leftPad(StringUtils.right(num, 4), StringUtils.length(num), "*"), "***")); } /** * [地址] 只显示到地区,不显示详细地址;我们要对个人信息增强保护<例子:北京市海淀区****> * * @param sensitiveSize 敏感信息长度 */ public static String address(final String address, final int sensitiveSize) { if (StringUtils.isBlank(address)) { return ""; } final int length = StringUtils.length(address); return StringUtils.rightPad(StringUtils.left(address, length - sensitiveSize), length, "*"); } /** * [电子邮箱] 邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示<例子:g**@163.com> */ public static String email(final String email) { if (StringUtils.isBlank(email)) { return ""; } final int index = StringUtils.indexOf(email, "@"); if (index <= 1) { return email; } else { return StringUtils.rightPad(StringUtils.left(email, 1), index, "*") .concat(StringUtils.mid(email, index, StringUtils.length(email))); } } /** * [银行卡号] 前六位,后四位,其他用星号隐藏每位1个星号<例子:6222600**********1234> */ public static String bankCard(final String cardNum) { if (StringUtils.isBlank(cardNum)) { return ""; } return StringUtils.left(cardNum, 6).concat(StringUtils.removeStart( StringUtils.leftPad(StringUtils.right(cardNum, 4), StringUtils.length(cardNum), "*"), "******")); } /** * [公司开户银行联号] 公司开户银行联行号,显示前两位,其他用星号隐藏,每位1个星号<例子:12********> */ public static String cnapsCode(final String code) { if (StringUtils.isBlank(code)) { return ""; } return StringUtils.rightPad(StringUtils.left(code, 2), StringUtils.length(code), "*"); } public static String sex(final String code) { if (StringUtils.isBlank(code)) { return ""; } return mixStr(code, '*', 0, 0); } public static String birthDay(final String code) { if (StringUtils.isBlank(code)) { return ""; } return mixStr(code, '*', 2, 0); } public static String mixStr(String str, char mixStr, int befor, int after) { if (str == null) { return null; } if (str.length() <= befor + after) { return str; } String p1 = ""; String p2 = ""; if (befor != 0) { p1 = str.substring(0, befor); str = str.substring(befor); } if (after != 0) { p2 = str.substring(str.length() - after, str.length()); str = str.substring(0,str.length() - after); } str = str.replaceAll(".", String.valueOf(mixStr)); str = p1 + str + p2; return str; } }
业务枚举:
package com.acxiom.crm5.common.core.desensitization; public enum SensitiveEnum { /** * 中文名 */ CHINESE_NAME, /** * 名 */ FIRST_NAME, /** * 姓 */ LAST_NAME, /** * 身份证号 */ ID_CARD, /** * 座机号 */ FIXED_PHONE, /** * 手机号 */ MOBILE_PHONE, /** * 地址 */ ADDRESS, /** * 电子邮件 */ EMAIL, /** * 银行卡 */ BANK_CARD, /** * 公司开户银行联号 */ CNAPS_CODE, /** * 性别 */ SEX, /** * 生日 */ BIRTHDAY }
效果: