springboot实现数据脱敏
-
怎么说呢,写着写着发觉 ”这写的什么玩意“ 。
总的来说就是,这篇文章并不能解决数据脱敏问题,但以下链接可以。
SpringBoot中利用自定义注解优雅地实现隐私数据脱敏
然后回到本文,本来是想基于AOP代理,实现返回数据脱敏的,具体流程是:
-
在controller做切面,实现返回通知
-
返回通知中获取返回值对象
-
利用反射获取返回值字段
-
标有脱敏注释的字段做脱敏处理
说着好像一套一套的,但事实上,忽略了一个重要的问题,复杂对象很难做反射,例如集合List,Set,Map,或者对象的引用也是对象,就算用多重判断深度遍历,但是反射带来的耗时以及空间开销都是值得思考的,总而言之,这是一个很low的方案。
但是我很少用到反射,并且感觉既然都写到这了,不如记录一下,aop和反射结合,以后可能会用来实现其他有趣的功能也说不定。那就记录一下吧。
-
1. 引入依赖
<!-- 引入aop支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2. 实现两个注解
一个标注在controller方法上,Service也可以
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DesensitizeResult {
}
一个标注在属性上
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DesensitizeField {
// 字段名称,给字段起个名字而已,没啥用
String name() default "字段名";
// 前面正常显示字符长度(不脱敏长度)
int prefixShow() default 1;
// 后面正常显示字符长度(不脱敏)长度
int suffixShow() default 0;
// 脱敏引用符号
String symbol() default "*";
}
3. 编写切面方法类
@Aspect
@Component
public class DesensitizeAspect {
@Pointcut("@annotation(com.example.testdemo.annotation.DesensitizeResult)")
public void getDesensitizeAnno(){}
/**
* 返回贴面编程,对返回结果result做脱敏操作
* @param joinPoint 切点
* @param result 目标方法返回结果
*/
@AfterReturning(pointcut = "getDesensitizeAnno()",returning = "result")
public void afterReturn(JoinPoint joinPoint,Object result){
// 获取返回结果类的所有属性数组
Class resultClass = result.getClass();
Field[] declaredFields = resultClass.getDeclaredFields();
for (Field field : declaredFields){
// 循环判断属性中是否存在自定义脱敏注解@DesensitizeField
for (Annotation annotation : field.getAnnotations()) {
Class<? extends Annotation> aClass = annotation.annotationType();
if (aClass.equals(DesensitizeField.class)){
DesensitizeField desensitizeField = (DesensitizeField) annotation;
// 对标有@DesensitizeField的属性进行脱敏处理
field.setAccessible(true); // 先将该属性改为允许值修改
try {
String originStr = (String)field.get(result); // 获取原来的值
// 字符串脱敏
String desensitizeValue = this.desensitizeStr(originStr, desensitizeField.prefixShow(),
desensitizeField.suffixShow(), desensitizeField.symbol());
field.set(result,desensitizeValue); // 将脱敏后的字符串写入
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
}
}
/**
*
* @param originStr 原字符串
* @param prefixShow 前置正常字符串长度
* @param suffixShow 后置正常字符串长度
* @param symbol 脱敏字符显示标志
* @return 脱敏后字符串
*/
private String desensitizeStr(String originStr, int prefixShow, int suffixShow, String symbol){
int length = originStr.length();
// 避免out of index
prefixShow = Math.min(prefixShow, length);
suffixShow = Math.min(suffixShow,length);
// 前后显示数据超过数据是指长度处理,OutOfRange处理
if (prefixShow+suffixShow>length){
prefixShow=length;
suffixShow=0;
}
String desensitizeValue = originStr.substring(0,prefixShow)
+symbol.repeat(length-prefixShow-suffixShow)
+originStr.substring(length-suffixShow);
return desensitizeValue;
}
}
如上代码所示,只是针对单个简单对象做字段脱敏,复杂对象就完了
4. 测试实体类
@Data
@ToString
public class UserInfo {
private String id;
private String account;
private String nickname;
@DesensitizeField(name = "真实姓名",prefixShow = 1)
private String realName;
@DesensitizeField(name = "密码",prefixShow = 0)
private String password;
@DesensitizeField(name = "手机号",prefixShow = 3,suffixShow = 1)
private String mobile;
@DesensitizeField(name = "身份证号",prefixShow = 3)
private String identityId;
private String createTime;
}
5. 测试接口
@RestController
public class TestController {
@DesensitizeResult // aop切面脱敏数据
@GetMapping("/userInfo")
public UserInfo getUserInfo(){
UserInfo userInfo = new UserInfo();
userInfo.setId("123456");
userInfo.setAccount("abcdef");
userInfo.setMobile("13579246810");
userInfo.setNickname("BigBoss");
userInfo.setRealName("张小凡");
userInfo.setIdentityId("430121200001011321");
userInfo.setPassword("Mm123456#");
return userInfo;
}
}
6.测试
至此,全文完毕,另外盘算着下次把上面链接的方案偷过来再发一篇。