SpringBoot 实现数据加密脱敏(注解 + 反射 + AOP)
场景:响应政府要求,商业软件应保证用户基本信息不被泄露
,不能直接展示
用户手机号,身份证,地址等敏感信息。
根据上面场景描述,我们可以分析出两个点。
- 不被
泄露
说明用户信息应被加密储存; - 不能
直接展示
说明用户信息应脱敏展示;
解决方案
-
傻瓜式编程:将项目中关于用户信息实体类的字段,比如姓名,手机号,身份证,地址等,在新增进数据库之前,对数据进行加密处理;在列表中展示用户信息时,对数据库中的数据进行解密脱敏,然后返回给前端;
-
切入式编程:将项目中关于用户信息实体类的字段用注解给标记,新增用户信息实体类(这里我们用UserBO来表示,给UserBO里面的name,phone字段添加@EncryptField),返回用户信息实体类(这里我们用UserDO来表示,给UserDO里面的name,phone字段添加@DecryptField);然后利用@EncryptField,@DecryptField做为切入点,以切面的形式实现加密,解密脱敏;
傻瓜式编程不是说傻,而是相当于切入式编程,傻瓜式编程需要对用户信息相关的所有接口
进行加密,解密脱敏的逻辑处理,这里改动的地方就比较多,风险高,重复操作相同的逻辑,工作量大,后期不好维护;切入式编程只需要对用户信息字段添加注解,对有注解的字段统一进行加密,解密脱敏逻辑处理,操作方便,高聚合,易维护;
方案实现
傻瓜式编程没什么难度,这里我给大家有切入式编程来实现;在实现之前,跟大家预热一下注解,反射,AOP的知识;
注解实战
创建注解
创建一个只能标记在方法上的注解:
package com.weige.javaskillpoint.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(ElementType.METHOD) //METHOD 说明该注解只能用在方法上
@Retention(RetentionPolicy.RUNTIME) //RUNTIME 说明该注解在运行时生效
public @interface Encryption {}
创建一个只能标记在字段上的注解:
package com.weige.javaskillpoint.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(ElementType.FIELD) //FIELD 说明该注解只能用在字段上
@Retention(RetentionPolicy.RUNTIME) //RUNTIME 说明该注解在运行时生效
public @interface EncryptField {}
创建一个标记在字段上,且有值的注解:
package com.weige.javaskillpoint.annotation;import com.weige.javaskillpoint.enums.DesensitizationEnum;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptField {// 注解是可以有值的,这里可以为数组,String,枚举等类型// DesensitizationEnum desensitizationEnum = field.getAnnotation(DecryptField.class).value(); 这里的field是指当前标记的字段DesensitizationEnum value();
}
注解使用
创建枚举
package com.weige.javaskillpoint.enums;public enum DesensitizationEnum {name, // 用户信息姓名脱敏address, // 用户信息地址脱敏phone; // 用户信息手机号脱敏
}
创建UserDO类
package com.weige.javaskillpoint.entity;import com.weige.javaskillpoint.annotation.DecryptField;
import com.weige.javaskillpoint.enums.DesensitizationEnum;
import com.weige.javaskillpoint.utils.AesUtil;import java.lang.reflect.Field;// 用户信息返回实体类
public class UserDO {@DecryptField(DesensitizationEnum.name)private String name;@DecryptField(DesensitizationEnum.address)private String address;public String getName() {return name;}public void setName(String name) {this.name = name;}public String getAddress() {return address;}public void setAddress(String address) {this.address = address;}public UserDO(String name, String address) {this.name = name;this.address = address;}public static void main(String[] args) throws IllegalAccessException {// 生成并初始化对象UserDO userDO = new UserDO("梦想是什么","湖北省武汉市");// 反射获取当前对象的所有字段Field[] fields = userDO.getClass().getDeclaredFields();// 遍历字段for (Field field : fields) {// 判断字段上是否存在@DecryptField注解boolean hasSecureField = field.isAnnotationPresent(DecryptField.class);// 存在if (hasSecureField) {// 暴力破解 不然操作不了权限为private的字段field.setAccessible(true);// 如果当前字段在userDo中不为空 即name,address字段有值if (field.get(userDO) != null) {// 获取字段上注解的value值DesensitizationEnum desensitizationEnum = field.getAnnotation(DecryptField.class).value();// 控制台输出System.out.println(desensitizationEnum);// 根据不同的value值 我们可以对字段进行不同逻辑的脱敏 比如姓名脱敏-魏*,手机号脱敏-187****2275 }}}}
}
反射实战
创建UserBO类
package com.weige.javaskillpoint.entity;import com.weige.javaskillpoint.annotation.EncryptField;import java.lang.reflect.Field;// 用户信息新增实体类
public class UserBO {@EncryptFieldprivate String name;@EncryptFieldprivate String address;public String getName() {return name;}public void setName(String name) {this.name = name;}public String getAddress() {return address;}public void setAddress(String address) {this.address = address;}public UserBO(String name, String address) {this.name = name;this.address = address;}@Overridepublic String toString() {return "UserBO{" +"name='" + name + '\'' +", address='" + address + '\'' +'}';}public static void main(String[] args) throws IllegalAccessException {UserBO userBO = new UserBO("周传雄","湖北省武汉市");Field[] fields = userBO.getClass().getDeclaredFields();for (Field field : fields) {boolean annotationPresent = field.isAnnotationPresent(EncryptField.class);if(annotationPresent){// 当前字段内容不为空if(field.get(userBO) != null){// 这里对字段内容进行加密Object obj = encrypt(field.get(userBO));// 字段内容加密过后 通过反射重新赋给该字段field.set(userBO, obj);}}}System.out.println(userBO);}public static Object encrypt(Object obj){return "加密: " + obj;}
}
AOP实战
切入点:
package com.weige.javaskillpoint.controller;import com.weige.javaskillpoint.annotation.Encryption;
import com.weige.javaskillpoint.entity.UserBO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/encrypt")
@Slf4j
public class EncryptController {@PostMapping("/v1")@Encryption // 切入点public UserBO insert(@RequestBody UserBO user) {log.info("加密后对象:{}", user);return user;}
}
切面:
package com.weige.javaskillpoint.aop;import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;@Slf4j
@Aspect
@Component
public class EncryptAspect {//拦截需加密注解 切入点@Pointcut("@annotation(com.weige.javaskillpoint.annotation.Encryption)")public void point() {}@Around("point()") //环绕通知public Object around(ProceedingJoinPoint joinPoint) throws Throwable {//加密逻辑处理encrypt(joinPoint);return joinPoint.proceed();}}
为什么这里要使用AOP:无论是注解,反射,都需要一个启动方法,我上面演示的是通过main函数来启动。使用AOP,项目启动后,只要调用切入点对应的方法,就会根据切入点来形成一个切面,进行统一的逻辑增强;如果大家熟悉SpringMVC,SpringMVC提供了 ResponseBodyAdvice 和 RequestBodyAdvice两个接口,这两个接口可以对请求和响应进行预处理,就可以不需要使用AOP;
加密解密脱敏实战
项目目录:
pom.xml文件:
<dependencies><!--Springboot项目自带 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!--Springboot Web项目 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.22</version></dependency><!-- hutool --><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.7.20</version></dependency><!-- 切面 aop --><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.7</version></dependency></dependencies>
实体类
用户信息新增实体类 :UserBO
package com.weige.javaskillpoint.entity;import com.weige.javaskillpoint.annotation.EncryptField;// 实体类
public class UserBO {@EncryptFieldprivate String name;@EncryptFieldprivate String address;public String getName() {return name;}public void setName(String name) {this.name = name;}public String getAddress() {return address;}public void setAddress(String address) {this.address = address;}public UserBO(String name, String address) {this.name = name;this.address = address;}@Overridepublic String toString() {return "UserBO{" +"name='" + name + '\'' +", address='" + address + '\'' +'}';}
}
用户信息返回实体类 :UserDO
package com.weige.javaskillpoint.entity;import com.weige.javaskillpoint.annotation.DecryptField;
import com.weige.javaskillpoint.enums.DesensitizationEnum;// 实体类
public class UserDO {@DecryptField(DesensitizationEnum.name)private String name;@DecryptField(DesensitizationEnum.address)private String address;public String getName() {return name;}public void setName(String name) {this.name = name;}public String getAddress() {return address;}public void setAddress(String address) {this.address = address;}public UserDO(String name, String address) {this.name = name;this.address = address;}
}
脱敏枚举
package com.weige.javaskillpoint.enums;public enum DesensitizationEnum {name,address,phone;
}
注解
解密字段注解(字段):
package com.weige.javaskillpoint.annotation;import com.weige.javaskillpoint.enums.DesensitizationEnum;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptField {DesensitizationEnum value();
}
解密方法注解(方法 作切入点):
package com.weige.javaskillpoint.annotation;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 Decryption {}
加密字段注解(字段):
package com.weige.javaskillpoint.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptField {}
加密方法注解(方法 作切入点):
package com.weige.javaskillpoint.annotation;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 Encryption {}
控制层
解密 Controller:
package com.weige.javaskillpoint.controller;import com.weige.javaskillpoint.annotation.Decryption;
import com.weige.javaskillpoint.entity.UserDO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/decrypt")
public class DecryptController {@GetMapping("/v1")@Decryptionpublic UserDO decrypt() {return new UserDO("7c29e296e92893476db5f9477480ba7f", "b5c7ff86ac36c01dda45d9ffb0bf73194b083937349c3901f571d42acdaa7bae");}}
加密 Controller:
package com.weige.javaskillpoint.controller;import com.weige.javaskillpoint.annotation.Encryption;
import com.weige.javaskillpoint.entity.UserBO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/encrypt")
@Slf4j
public class EncryptController {@PostMapping("/v1")@Encryptionpublic UserBO insert(@RequestBody UserBO user) {log.info("加密后对象:{}", user);return user;}
}
切面
解密脱敏切面:
package com.weige.javaskillpoint.aop;import com.weige.javaskillpoint.annotation.DecryptField;
import com.weige.javaskillpoint.enums.DesensitizationEnum;
import com.weige.javaskillpoint.utils.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;@Slf4j
@Aspect
@Component
public class DecryptAspect {//拦截需解密注解@Pointcut("@annotation(com.weige.javaskillpoint.annotation.Decryption)")public void point() {}@Around("point()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {//解密return decrypt(joinPoint);}public Object decrypt(ProceedingJoinPoint joinPoint) {Object result = null;try {Object obj = joinPoint.proceed();if (obj != null) {//抛砖引玉 ,可自行扩展其他类型字段的判断if (obj instanceof String) {decryptValue();} else {result = decryptData(obj);}}} catch (Throwable e) {e.printStackTrace();}return result;}private Object decryptData(Object obj) throws IllegalAccessException {if (Objects.isNull(obj)) {return null;}if (obj instanceof ArrayList) {decryptList(obj);} else {decryptObj(obj);}return obj;}private void decryptObj(Object obj) throws IllegalAccessException {Field[] fields = obj.getClass().getDeclaredFields();for (Field field : fields) {boolean hasSecureField = field.isAnnotationPresent(DecryptField.class);if (hasSecureField) {field.setAccessible(true);if (field.get(obj) != null) {String realValue = (String) field.get(obj);DesensitizationEnum desensitizationEnum = field.getAnnotation(DecryptField.class).value();String value = (String) AesUtil.decrypt(realValue,desensitizationEnum);field.set(obj, value);}}}}private void decryptList(Object obj) throws IllegalAccessException {List<Object> result = new ArrayList<>();if (obj instanceof ArrayList) {result.addAll((Collection<?>) obj);}for (Object object : result) {decryptObj(object);}}private void decryptValue() {log.info("根据对象进行解密脱敏,单个字段不做处理!");}}
加密切面:
package com.weige.javaskillpoint.aop;import com.weige.javaskillpoint.annotation.EncryptField;
import com.weige.javaskillpoint.entity.UserBO;
import com.weige.javaskillpoint.utils.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;import java.lang.reflect.Field;@Slf4j
@Aspect
@Component
public class EncryptAspect {//拦截需加密注解@Pointcut("@annotation(com.weige.javaskillpoint.annotation.Encryption)")public void point() {}@Around("point()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {//加密encrypt(joinPoint);return joinPoint.proceed();}public void encrypt(ProceedingJoinPoint joinPoint) {Object[] objects;try {objects = joinPoint.getArgs();if (objects.length != 0) {for (Object object : objects) {if (object instanceof UserBO) {Field[] fields = object.getClass().getDeclaredFields();for (Field field : fields) {if (field.isAnnotationPresent(EncryptField.class)) {field.setAccessible(true);if (field.get(object) != null) {// 进行加密Object o = field.get(object);Object encrypt = AesUtil.encrypt(field.get(object));field.set(object, encrypt);}}}}}}} catch (Exception e) {log.error(e.getMessage());}}
}
工具类
加密工具类:AesUtil
package com.weige.javaskillpoint.utils;import cn.hutool.core.util.CharsetUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import com.weige.javaskillpoint.enums.DesensitizationEnum;public class AesUtil {// 默认16位 或 128 256位public static String AES_KEY = "Wk#qerdfdshbd910";public static AES aes = SecureUtil.aes(AES_KEY.getBytes());public static Object encrypt(Object obj) {return aes.encryptHex((String) obj);}public static Object decrypt(Object obj, DesensitizationEnum desensitizationEnum) {// 解密Object decrypt = decrypt(obj);// 脱敏return DesensitizationUtil.desensitization(decrypt, desensitizationEnum);}public static Object decrypt(Object obj) {return aes.decryptStr((String) obj, CharsetUtil.CHARSET_UTF_8);}}
脱敏工具类:DesensitizationUtil
package com.weige.javaskillpoint.utils;import cn.hutool.core.util.StrUtil;
import com.weige.javaskillpoint.enums.DesensitizationEnum;public class DesensitizationUtil {public static Object desensitization(Object obj, DesensitizationEnum desensitizationEnum) {Object result;switch (desensitizationEnum) {case name:result = strUtilHide(obj, 1);break;case address:result = strUtilHide(obj, 3);break;default:result = "";}return result;}/*** start从0开始*/public static Object strUtilHide(String obj, int start, int end) {return StrUtil.hide(obj, start, end);}public static Object strUtilHide(Object obj, int start) {return strUtilHide(((String) obj), start, ((String) obj).length());}}
完结
以上代码不难,大伙复制到本地跑一遍,基本就能理解;愿每一位程序员少走弯路!