1.引言
继上一篇文章:# springboot框架项目实践应用一(敏感信息脱敏)。简单分享了如何实现数据库连接配置密码加密脱敏。实际项目中,我们有更多需要脱敏的应用需求。比如:
- 用户注册的时候,需要将用户手机号、邮箱地址脱敏处理后,保存数据库;
- 当用户获取个人信息的时候,即根据用户Id查询存储在数据库中的数据,最后展示的时候,要将用户手机号、邮箱地址还原
要如何才能实现这样的需求?那么今天这篇文章,我们就从这个需求出发,看看如何实现?
2.需求分析
我们先来整理一下需求
- 用户注册,提供用户信息:用户名称、用户年龄、用户手机号码、用户邮箱地址等
- 用户注册成功,将用户信息持久化保存到数据库表中
- 其中用户手机号码、用户邮箱地址属于敏感信息,需要脱敏处理,即数据库中保存的是加密后的数据
- 用户查询个人信息,从数据库表中根据用户Id获取个人基本信息,包含:用户名称、用户年龄、用户手机号码、用户邮箱地址
- 前端展示用户信息,需要将脱敏后的用户手机号码,用户邮箱地址还原
假设用户VO对象如下:
java
/**
* 用户VO
*
* @author ThinkPad
* @version 1.0
*/
@Data
public class UserVO {
private Long userId;
private String userName;
private Integer age;
private String phone;
private String email;
}
需求其实非常明确,就是如何将phone、email脱敏处理。有小伙伴可能会说,简单呀!只需要
- 在注册用户方法中,保存数据库前,将UserVO中的phone、email加密
- 在查询用户信息方法中,将查询到的UserVO中phone、email解密
这么一来,实现还是比较简单的!能实现。但是在实际项目中,我们需要思考这么些问题,你比如说 业务上,有很多地方都需要用户信息
- 意味着每个地方都需要有加密、解密相关的代码--->导致代码混乱,不优雅,哪一天要是需求变了,说不需要再对用户信息进行脱敏处理--->到处删代码,难以维护
- 业务上,除了用户信息,还有其它信息要脱敏处理
等等类似需求,有没有更加优雅的实现方式呢?答案是有的,通过aop将脱敏的代码统一收敛起来,不就可以了吗,对吧
下面我们一起来看具体实现过程。
3.需求实现
3.1.导入依赖
xml
<!--aop依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--加解密依赖-->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
3.2.编写注解
为了让代码实现起来优雅,方便维护,通过注解标注的实现方式,是无比美好的!我们定义两个注解
- JasyptMethod注解:标注方法,有该注解的方法说明需要脱敏处理
- JasyptField注解:标注成员变量,有该注解的成员变量,才需要脱敏处理
注解JasyptMethod
java
/**
* 脱敏方法标注
*
* @author ThinkPad
* @version 1.0
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface JasyptMethod {
}
注解JasyptField
java
/**
* 脱敏字段标注
*
* @author ThinkPad
* @version 1.0
*/
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface JasyptField {
}
修改UserVO
将UserVO中,成员变量phone、email上增加注解JasyptField
java
/**
* 用户VO
*
* @author ThinkPad
* @version 1.0
*/
@Data
public class UserVO {
private Long userId;
private String userName;
private Integer age;
/**
* 需要脱敏处理的成员变量
*/
@JasyptField
private String phone;
@JasyptField
private String email;
}
编写切面
编写切面JasyptAspect,统一处理脱敏。代码我就不解释了,关键代码地方我加了注释,相信熟悉aop的小伙伴都能看的明白
java
/**
* 脱敏处理切面,根据JasyptMethod、JasyptField注解进行处理
*
* @author ThinkPad
* @version 1.0
*/
@Component
@Aspect
@Slf4j
public class JasyptAspect {
/**
* 注入加解密 Encryptor
*/
@Autowired
private StringEncryptor stringEncryptor;
@Pointcut("@annotation(cn.edu.anan.aop.JasyptMethod)")
public void jasyptPointCut() {
}
/**
* 脱敏处理通知-环绕通知
* @param pjp
* @return
*/
@Around("jasyptPointCut()")
public Object jasyptAround(ProceedingJoinPoint pjp) {
// 1.参数加密处理
encrypt(pjp);
// 2.返回值解密处理
Object result = decrypt(pjp);
// 响应前,还原数据
log.info("3.响应前,还原数据:{}", result);
return result;
}
/**
* 封装加密处理
* @param pjp
*/
private void encrypt(ProceedingJoinPoint pjp){
try {
Object[] objects = pjp.getArgs();
if (objects.length != 0) {
for (Object o : objects) {
// 简单类型加密
if (o instanceof String) {
o = stringEncryptor.encrypt(String.valueOf(o));
} else {
// 引用类型加密
if(Objects.nonNull(o)){
// 打印输出原参数
log.info("1.脱敏前,原请求参数:{}", o);
// 脱敏
Field[] fields = o.getClass().getDeclaredFields();
for(Field f : fields){
boolean yes = f.isAnnotationPresent(JasyptField.class);
if(yes){
f.setAccessible(true);
String realValue = (String)f.get(o);
String encryptValue = stringEncryptor.encrypt(realValue);
f.set(o,encryptValue);
}
}
}
}
}
}
} catch (IllegalAccessException e) {
log.error("jasypt 加密处理异常,异常消息:{}", e.getMessage());
}
}
/**
* 封装解密处理
* @param pjp
* @return
*/
private Object decrypt(ProceedingJoinPoint pjp) {
Object result = null;
try {
result = pjp.proceed();
if (Objects.nonNull(result)) {
if (result instanceof String) {
result = stringEncryptor.decrypt(String.valueOf(result));
} else {
Field[] fields = result.getClass().getDeclaredFields();
for(Field f : fields){
boolean yes = f.isAnnotationPresent(JasyptField.class);
if(yes){
f.setAccessible(true);
String encryptValue = (String)f.get(result);
String realValue = stringEncryptor.decrypt(encryptValue);
f.set(result,realValue);
}
}
}
}
} catch (Throwable e) {
log.error("jasypt 解密处理异常,异常消息:{}", e.getMessage());
}
return result;
}
}
测试效果
在JasyptController中,增加一个测试方法
java
/**
* 请求参数脱敏处理
* @param vo
* @return
*/
@RequestMapping("encrypt")
@JasyptMethod
public UserVO encrypt(UserVO vo){
log.info("2.脱敏后的数据:{}", vo);
return vo;
}
浏览器响应结果:
从应用控制台,我们看到
- 脱敏前,phone、email都是用户提供的明文
- 脱敏后,phone、email是加密后的密文--->项目中,对应我们持久化到数据库的用户信息
- 响应前,phone、email还原成了明文--->项目中,对应我们从数据库查询用户信息,展示给用户