前言
相信大家都有这样一个烦恼,就是经常会接到各种推销、广告的电话和短信,如果你没有在他那里留下过联系方式,他又是如何得到了你的联系方式呢?毫无疑问,是个人信息被泄漏了。个人信息的泄漏有人为不合法谋利的因素,也有系统不合理的安全设计造成泄漏的因素。当然系统设计的角度出发,敏感信息需要加密存储的,数据展示的时候也要进行相应的脱敏处理,但是从一些关于个信息泄漏的新闻报道来看,有好多的网站后台竟然是“裸奔”状态,简直太可怕了。其实敏感数据的处理也不复杂,说到底是安全意识不强。当然,这篇文章和大家分享的重点是加密和解密的方法,不是数据安全的重要性。
基本概念
敏感数据
敏感数据是指那些泄漏后可能会给社会或个人造成严重危害的数据,以个人隐私信息为例,如手机号码、家庭住址、邮箱、身份证号、银行卡帐号、购物网站的支付密码、登陆密码等等。另外从社会的角度出发,也有很多数据是属于敏感数据,如:居民的生物基因信息等等。
数据加密
数据加密是指对数据重新编码来保护数据,获取实际数据的唯一办法就是使用密钥解密数据;
数据解密
数据解密与数据加密是相对的,即使用密钥对加密的数据进行解密的过程;
加密方式
加密的方式,一般是两种:对称加密和非对称加密;
对称加密只有一个秘钥,加密和解密都是用同一个秘钥,如AES、DES等;
非对称加密有两个秘钥,一个是公钥,一个是私钥。使用公钥对数据进行加密,加密后的数据只有私钥可以解密,一般公钥是公开的,私钥是不公开的;如RSA、DSA等;
实现原理
Springboot项目中,客户端通过接口向服务端读取或写入敏感数据时,常会有这样的业务需求:
1、在客户端向服务器端发起写入请求,服务端需要对写入的敏感数据进行加密后存储;
2、在客户端从服务器端向外读取数据的时候,需要对输出的敏感数据进行解密;
显然这种场景,对于加密的方式的选择,对称加密是最好的选择;那么如何实现对写入请求、读取请求的敏感数据的加密、解密处理呢?解决方案如下:
1、自定义两个切面注解,分别是加密切面注解、解密切面注解,作用于需要加密或解密的敏感数据处理的业务处理类的具体业务处理方法上;
2、自定义两个敏感字段处理注解,分别是加密字段注解、解密字段注解,作用于需要输入或输出的对象的敏感字段上;如果输入对象上标记了加密字段注解,则表示该字段在对内写入数据库的时候,需要加密处理;同理,如果输出对象上标记了解密字段注解,则表示该字段在对外输出的时候,需要进行解密;
3、使用面向切面编程,定义两个切面类,分别是加密切面类和解密切面类,选择Spring AOP的环绕通知来具体实现;加密切面类中,以注解的方式定义切入点,用到的注解就是自定义的加密切面注解;
4、如果新增、编辑等写入类的业务请求处理方法上标记了加密切面注解,那么写入请求在正式被业务处理方法处理前,会命中加密切面类,加密切面类的环绕通知方法被触发,然后根据输入的参数对象中的字段是否标记了自定义的加密字段注解,来决定是否对当前字段进行加密处理;
5、同理,如果是查询等读取类的业务请求处理方法上标记了解密切面注解,那么读取请求被业务处理类处理完之后,会命中解密切面类,解密切面类的环绕通知方法被触发,然后根据返回对象的字段是否标记了解密字段注解,来决定是否对当前字段进行解密处理。
实现方案
环境配置
jdk版本:1.8开发工具:Intellij iDEA 2020.1
springboot:2.3.9.RELEASE
mybatis-spring-boot-starter:2.1.4
依赖配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.3</version>
</dependency>
示例时序图
图片
示例代码
1、自定义四个注解:@DecryptField(解密字段注解)、@EncryptField(加密字段注解)、@NeedEncrypt(解密切面注解)、@NeedEncrypt(加密切面注解),其中@DecryptField作用于需要解密的字段上;@EncryptField作用于需要加密的字段上;@NeedEncrypt作用于需要对入参数进行加密处理的方法上;@NeedDecrypt作用于需要对返回值进行解密处理的方法上;
//解密字段注解
@Target(value = {ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptField {
}
//加密字段注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptField {
}
//作用于对返回值进行解密处理的方法上
@Target(value = {ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NeedDecrypt {
}
//作用于需要对入参数进行加密处理的方法上
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NeedEncrypt {
}
2、把自定义的加密字段注解、解密字段注解标记在需要加密或者解密的字段上;这里表示在写入人员的手机号码、身份证号码、家庭住址门牌号码时,要进行加密处理;在读取人员的手机号码、身份证号码、家庭住址门牌号码时,要进行解密处理;
@Slf4j
@Data
public class Person {
private Integer id;
private String userName;
private String loginNo;
@EncryptField
@DecryptField
private String phoneNumber;
private String sex;
@DecryptField
@EncryptField
private String IDCard;
private String address;
@EncryptField
@DecryptField
private String houseNumber;
}
3、把@NeedEncrypt和@NeedDecrypt标记在需要对入参数、返回值中的敏感字段进行加密、解密处理的业务处理方法上;
@RestController
@RequestMapping("/person")
@Slf4j
public class PersonController {
@Autowired
private IPersonService personService;
//添加人员信息
@PostMapping("/add")
@NeedEncrypt
public Person add(@RequestBody Person person, Model model) {
Person result = this.personService.registe(person);
log.info("//增加person执行完成");
return result;
}
//人员信息列表查询
@GetMapping("/list")
@NeedDecrypt
public List<Person> getPerson() {
List<Person> persons = this.personService.getPersonList();
log.info("//查询person列表执行完成");
return persons;
}
//人员信息详情查询
@GetMapping("/{id}")
@NeedDecrypt
public Person get(@PathVariable Integer id) {
Person persnotallow= this.personService.get(id);
log.info("//查询person详情执行完成");
return person;
}
}
4、自定义加密切面类(EncryptAop)和解密切面类(DecryptAop):用@NeedEncrypt注解定义加密切点,在加密切点的环绕通知方法里执行到具体的业务处理方法之前,判断输入对象的参数字段是否标记了@EncryptField(加密字段注解),如果判断结果为true,则使用java反射对该字段进行加密处理,注意这里引用了hutool的工具包,使用了工具包里的加密和解密方法,这里也可以替换成其他的方式;用@NeedDecrypt注解定义解密切点,在解密切点的环绕通知方法里执行完具体的业务处理方法之后,判断输出对象的参数字段是否标记了@DecryptField(解密字段注解),如果判断结果为true,则使用java反射对该 字段进行解密处理;
@Component
@Aspect
@Slf4j
public class EncryptAop {
/**
* 定义加密切入点
*/
@Pointcut(value = "@annotation(com.fanfu.anno.NeedEncrypt)")
public void pointcut() {
}
/**
* 命中加密切入点的环绕通知
*
* @param proceedingJoinPoint
* @return
* @throws Throwable
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("//环绕通知 start");
//获取命中目标方法的入参数
Object[] args = proceedingJoinPoint.getArgs();
if (args.length > 0) {
for (Object arg : args) {
//按参数的类型进行判断,如果业务中还有其他的类型,可酌情增加
if (arg != null) {
if (arg instanceof List) {
for (Object tmp : ((List) arg)) {
//加密处理
this.deepProcess(tmp);
}
} else {
this.deepProcess(arg);
}
}
}
}
//对敏感数据加密后执行目标方法
Object result = proceedingJoinPoint.proceed();
log.info("//环绕通知 end");
return result;
}
public void deepProcess(Object obj) throws IllegalAccessException {
if (obj != null) {
//获取对象的所有字段属性并遍历
Field[] declaredFields = obj.getClass().getDeclaredFields();
for (Field declaredField : declaredFields) {
//判断字段属性上是否标记了@EncryptField注解
if (declaredField.isAnnotationPresent(EncryptField.class)) {
//如果判断结果为真,则取出字段属性值,进行加密、重新赋值
declaredField.setAccessible(true);
Object valObj = declaredField.get(obj);
if (valObj != null) {
String value = valObj.toString();
//开始敏感字段属性值加密
String decrypt = this.encrypt(value);
//把加密后的字段属性值重新赋值
declaredField.set(obj, decrypt);
}
}
}
}
}
private String encrypt(String value) {
//这里特别注意一下,对称加密是根据密钥进行加密和解密的,加密和解密的密钥是相同的,一旦泄漏,就无秘密可言,
//“fanfu-csdn”就是我自定义的密钥,这里仅作演示使用,实际业务中,这个密钥要以安全的方式存储;
byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DES.getValue(), "fanfu-csdn".getBytes()).getEncoded();
SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.DES, key);
String encryptValue = aes.encryptBase64(value);
return encryptValue;
}
}
@Component
@Aspect
@Slf4j
public class DecryptAop {
/**
* 定义需要解密的切入点
*/
@Pointcut(value = "@annotation(com.fanfu.anno.NeedDecrypt)")
public void pointcut() {
}
/**
* 命中的切入点时的环绕通知
*
* @param proceedingJoinPoint
* @return
* @throws Throwable
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("//环绕通知 start");
//执行目标方法
Object result = proceedingJoinPoint.proceed();
//判断目标方法的返回值类型
if (result instanceof List) {
for (Object tmp : ((List) result)) {
//数据脱敏处理逻辑
this.deepProcess(tmp);
}
} else {
this.deepProcess(result);
}
log.info("//环绕通知 end");
return result;
}
public void deepProcess(Object obj) throws IllegalAccessException {
if (obj != null) {
//取出输出对象的所有字段属性,并遍历
Field[] declaredFields = obj.getClass().getDeclaredFields();
for (Field declaredField : declaredFields) {
//判断字段属性上是否标记DecryptField注解
if (declaredField.isAnnotationPresent(DecryptField.class)) {
//如果判断结果为真,则取出字段属性数据进行解密处理
declaredField.setAccessible(true);
Object valObj = declaredField.get(obj);
if (valObj != null) {
String value = valObj.toString();
//加密数据的解密处理
value = this.decrypt(value);
DecryptField annotation = declaredField.getAnnotation(DecryptField.class);
boolean open = annotation.open();
//把解密后的数据重新赋值
declaredField.set(obj, value);
}
}
}
}
}
private String decrypt(String value) {
//这里特别注意一下,对称加密是根据密钥进行加密和解密的,加密和解密的密钥是相同的,一旦泄漏,就无秘密可言,
//“fanfu-csdn”就是我自定义的密钥,这里仅作演示使用,实际业务中,这个密钥要以安全的方式存储;
byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DES.getValue(), "fanfu-csdn".getBytes()).getEncoded();
SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.DES, key);
String decryptStr = aes.decryptStr(value);
return decryptStr;
}
}
加密结果
图片
解密结果
图片
总结
这篇着重和大家分享的内容如下:
1、敏感数据的一些基础概念;
2、敏感数据处理的解决思路;
3、敏感数据处理的具体实现方式;