响应政府要求,商业软件应保证用户基本信息不被泄露,不能直接展示用户手机号,身份证,地址等敏感信息。
根据上面场景描述,我们可以分析出两个点。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
-
项目地址:https://github.com/YunaiV/ruoyi-vue-pro
-
视频教程:https://doc.iocoder.cn/video/
-
傻瓜式编程:
将项目中关于用户信息实体类的字段,比如姓名,手机号,身份证,地址等,在新增进数据库之前,对数据进行加密处理;在列表中展示用户信息时,对数据库中的数据进行解密脱敏,然后返回给前端;
-
切入式编程:
将项目中关于用户信息实体类的字段用注解给标记,新增用户信息实体类(这里我们用UserBO来表示,给UserBO里面的name,phone字段添加
@EncryptField
),返回用户信息实体类(这里我们用UserDO来表示,给UserDO里面的name,phone字段添加
@DecryptField
);然后利用
@EncryptField
,
@DecryptField
做为切入点,以切面的形式实现加密,解密脱敏;
傻瓜式编程不是说傻,而是相当于切入式编程,傻瓜式编程需要对用户信息相关的所有接口进行加密,解密脱敏的逻辑处理,这里改动的地方就比较多,风险高,重复操作相同的逻辑,工作量大,后期不好维护;切入式编程只需要对用户信息字段添加注解,对有注解的字段统一进行加密,解密脱敏逻辑处理,操作方便,高聚合,易维护;
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
-
项目地址:https://github.com/YunaiV/yudao-cloud
-
视频教程:https://doc.iocoder.cn/video/
傻瓜式编程没什么难度,这里我给大家有切入式编程来实现;在实现之前,跟大家预热一下注解,反射,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 {
@EncryptField
private String name;
@EncryptField
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 UserBO(String name, String address) {
this.name = name;
this.address = address;
}
@Override
public 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;
}
}
切入点:
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文件:
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
1.18.22
cn.hutool
hutool-all
5.7.20
org.aspectj
aspectjweaver
1.9.7
用户信息新增实体类 :UserBO
package com.weige.javaskillpoint.entity;
import com.weige.javaskillpoint.annotation.EncryptField;
// 实体类
public class UserBO {
@EncryptField
private String name;
@EncryptField
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 UserBO(String name, String address) {
this.name = name;
this.address = address;
}
@Override
public 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")
@Decryption
public 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")
@Encryption
public 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()"