SpringBoot实现数据脱敏
文章目录
- `SpringBoot`实现数据脱敏
- 一、基于实体类属性的 `get`/`set` 方法
- 二、基于 `MyBatis TypeHandler`
- 三、基于实体类属性注解与 `Spring AOP`
- 四、基于实体类属性注解与 `Jackson` 序列化机制
SpringBoot
实现数据脱敏
一、基于实体类属性的 get
/set
方法
优点:
- 实现简单,易于理解
- 无侵入业务逻辑
- 统一控制,便于维护
缺点:
- 需要大量重写
get
/set
方法 - 只适用查询,不适用
新增/修改
二、基于 MyBatis TypeHandler
注:
TypeHandler
更适用于:加解密敏感字段,安全性高,且业务逻辑不受影响
优点:
- 可同时实现加密存入,脱敏取出
- 无侵入业务逻辑
- 统一控制,便于维护
缺点:
- 需要大量重写
resultMap
返回结果类型 - 脱敏后业务层无法使用原始数据
示例:
自定义
TypeHandler
后,在MyBatis
的映射文件中通过typeHandler
属性引用即可生效。
注:IdCardUtils
可自行实现,此处不做展示
public class IdCardTypeHandler extends BaseTypeHandler<String> {@Overridepublic void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {ps.setString(i, IdCardUtils.publicEncrypt(parameter)); // 在设置参数时加密}@Overridepublic String getNullableResult(ResultSet rs, String columnName) throws SQLException {return IdCardUtils.privateDecrypt(rs.getString(columnName)); // 查询结果时解密,脱敏}@Overridepublic String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {return IdCardUtils.privateDecrypt(rs.getString(columnIndex)); // 读取:按列名}@Overridepublic String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {return IdCardUtils.privateDecrypt(cs.getString(columnIndex)); // 读取:存储过程}
}
<resultMap id="UserResult" type="User"><result property="idCard" column="id_card" typeHandler="IdCardTypeHandler"/>
</resultMap>
三、基于实体类属性注解与 Spring AOP
优点:
- 可扩展性强
- 无侵入业务逻辑
- 统一控制,便于维护
缺点:
AOP
代理限制,避免方法在同类中调用- 不适用于基本类型数组和 Map
- 嵌套对象需递归处理
示例:
1、枚举脱敏类型、定义注解、工具类,参考第4种方式中实现,此处不做重复展示
2、实现切面类
注:不是
springboot
版本需手动配置AOP
,扫描范围根据实际情况自行配置
<!-- 1. 扫描组件:Service、Repository、Aspect 等 -->
<context:component-scan base-package="com.module" /><!-- 2. 启用 AspectJ 自动代理(AOP 生效的关键) -->
<aop:aspectj-autoproxy proxy-target-class="true" />
@Aspect
@Component
public class SensitiveDataAspect //切面类实现脱敏逻辑
{ /*** 拦截所有 Service 方法的返回值*/@AfterReturning(pointcut = "execution(* com.service..*.*(..))", returning = "result")public void desensitize(Object result){if (result == null) return;if (result instanceof Collection){Collection<?> collection = (Collection<?>)result;for (Object obj : collection){desensitizeObject(obj);}}else if (result.getClass().isArray()){Object[] array = (Object[])result;for (Object obj : array){desensitizeObject(obj);}}else{desensitizeObject(result);}}private void desensitizeObject(Object obj){if (obj == null) return;Class<?> clazz = obj.getClass();Field[] fields = clazz.getDeclaredFields();for (Field field : fields){try{Sensitive sensitive = field.getAnnotation(Sensitive.class);if (sensitive != null){field.setAccessible(true);Object value = field.get(obj);if (value instanceof String){String masked = sensitive.value().desensitizer().apply((String) value);field.set(obj, masked);} else {// 递归处理嵌套对象desensitizeObject(value);}}}catch (Exception e){//处理日志。。。}}}
}
3、在实体类属性中加上注解
@Sensitive(SensitiveType.PHONE)
private String userName;//该字段需要脱敏,加上注解
四、基于实体类属性注解与 Jackson
序列化机制
优点:
- 脱敏时机精准
- 性能更优
- 天然支持嵌套结构
缺点:
- 仅对
JSON
有效 - 不适用于非
Jackson
场景
示例:
1、枚举脱敏类型
public enum DesensitizedType
{/*** 姓名,第2位星号替换*/USERNAME(s -> s.replaceAll("(\\S)\\S(\\S*)", "$1*$2")),/*** 密码,全部字符都用*代替*/PASSWORD(DesensitizedUtil::password),/*** 身份证,中间10位星号替换*/ID_CARD(s -> s.replaceAll("(\\d{4})\\d{10}(\\d{3}[Xx]|\\d{4})", "$1** **** ****$2")),/*** 手机号,中间4位星号替换*/PHONE(s -> s.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")),/*** 电子邮箱,仅显示第一个字母和@后面的地址显示,其他星号替换*/EMAIL(s -> s.replaceAll("(^.)[^@]*(@.*$)", "$1****$2")),/*** 银行卡号,保留最后4位,其他星号替换*/BANK_CARD(s -> s.replaceAll("\\d{15}(\\d{3})", "**** **** **** **** $1")),/*** 车牌号码,包含普通车辆、新能源车辆*/CAR_LICENSE(DesensitizedUtil::carLicense);private final Function<String, String> desensitizer;DesensitizedType(Function<String, String> desensitizer){this.desensitizer = desensitizer;}public Function<String, String> desensitizer(){return desensitizer;}
}
2、脱敏工具类
public class DesensitizedUtil
{/*** 密码的全部字符都用*代替,比如:******** @param password 密码* @return 脱敏后的密码*/public static String password(String password){if (StringUtils.isBlank(password)){return StringUtils.EMPTY;}return StringUtils.repeat('*', password.length());}/*** 车牌中间用*代替,如果是错误的车牌,不处理** @param carLicense 完整的车牌号* @return 脱敏后的车牌*/public static String carLicense(String carLicense){if (StringUtils.isBlank(carLicense)){return StringUtils.EMPTY;}// 普通车牌if (carLicense.length() == 7){carLicense = StringUtils.hide(carLicense, 3, 6);}else if (carLicense.length() == 8){// 新能源车牌carLicense = StringUtils.hide(carLicense, 3, 7);}return carLicense;}/*** 根据输入的电话号码字符串,返回格式化后的电话号码字符串* 格式化后的电话号码将隐藏中间四位数字,以保护用户隐私* 如果输入的电话号码为空或不符合中国大陆电话号码的常见格式(11位数字),则返回空字符串** @param phoneNumber 用户输入的电话号码字符串* @return 格式化后的电话号码字符串,如果输入不合法,则返回空字符串*/public static String phoneNumber(String phoneNumber){if (StringUtils.isBlank(phoneNumber) || !Pattern.matches("^\\d{11}$", phoneNumber)) {return StringUtils.EMPTY;}return phoneNumber.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");}
}
3、定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveJsonSerializer.class)
public @interface Sensitive
{DesensitizedType desensitizedType();
}
4、自定义序列化
public class SensitiveJsonSerializer extends JsonSerializer<String> implements ContextualSerializer
{private DesensitizedType desensitizedType;@Overridepublic void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException{gen.writeString(desensitizedType.desensitizer().apply(value));}@Overridepublic JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)throws JsonMappingException{Sensitive annotation = property.getAnnotation(Sensitive.class);if (Objects.nonNull(annotation) && Objects.equals(String.class, property.getType().getRawClass())){this.desensitizedType = annotation.desensitizedType();return this;}return prov.findValueSerializer(property.getType(), property);}
}
5、在实体类属性中加上注解
@Sensitive(SensitiveType.PHONE)
private String userName;//该字段需要脱敏,加上注解