大数跨境
0
0

SpringBoot 实现数据加密脱敏

SpringBoot 实现数据加密脱敏 终码一生
2025-08-27
0
点击“终码一生”,关注,置顶公众号
每日技术干货,第一时间送达!
为了保证用户基本信息不被泄露,应用不能直接展示用户手机号,身份证,地址等敏感信息。
根据上面场景描述,我们可以分析出两个点。
  • 不被泄露说明用户信息应被加密储存;
  • 不能直接展示说明用户信息应脱敏展示;
01
解决方案
傻瓜式编程: 将项目中关于用户信息实体类的字段,比如姓名,手机号,身份证,地址等,在新增进数据库之前,对数据进行加密处理;在列表中展示用户信息时,对数据库中的数据进行解密脱敏,然后返回给前端;
切入式编程: 将项目中关于用户信息实体类的字段用注解给标记,新增用户信息实体类(这里我们用UserBO来表示,给UserBO里面的name,phone字段添加@EncryptField),返回用户信息实体类(这里我们用UserDO来表示,给UserDO里面的name,phone字段添加@DecryptField);然后利用@EncryptField@DecryptField做为切入点,以切面的形式实现加密,解密脱敏;
傻瓜式编程不是说傻,而是相当于切入式编程,傻瓜式编程需要对用户信息相关的所有接口进行加密,解密脱敏的逻辑处理,这里改动的地方就比较多,风险高,重复操作相同的逻辑,工作量大,后期不好维护;切入式编程只需要对用户信息字段添加注解,对有注解的字段统一进行加密,解密脱敏逻辑处理,操作方便,高聚合,易维护;
02
方案实现
傻瓜式编程没什么难度,这里我给大家有切入式编程来实现;在实现之前,跟大家预热一下注解,反射,AOP的知识;
注解实战
创建注解
创建一个只能标记在方法上的注解:
@Target(ElementType.METHOD)         //METHOD 说明该注解只能用在方法上
@Retention(RetentionPolicy.RUNTIME) //RUNTIME 说明该注解在运行时生效
public@interface Encryption {

}
创建一个只能标记在字段上的注解:
@Target(ElementType.FIELD)           //FIELD 说明该注解只能用在字段上
@Retention(RetentionPolicy.RUNTIME)  //RUNTIME 说明该注解在运行时生效
public@interface EncryptField {

}
创建一个标记在字段上,且有值的注解:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public@interface DecryptField {
// 注解是可以有值的,这里可以为数组,String,枚举等类型
// DesensitizationEnum desensitizationEnum = field.getAnnotation(DecryptField.class).value(); 这里的field是指当前标记的字段
    DesensitizationEnum value()
}
注解使用
创建枚举
publicenum DesensitizationEnum {
    name,     // 用户信息姓名脱敏
    address,  // 用户信息地址脱敏
    phone;    // 用户信息手机号脱敏
}
创建UserDO类
// 用户信息返回实体类
publicclassUserDO{

    @DecryptField(DesensitizationEnum.name)
    private String name;

    @DecryptField(DesensitizationEnum.address)
    private String address;

    public String getName(){
        return name;
    }

    publicvoidsetName(String name){
        this.name = name;
    }

    public String getAddress(){
        return address;
    }

    publicvoidsetAddress(String address){
        this.address = address;
    }

    publicUserDO(String name, String address){
        this.name = name;
        this.address = address;
    }

    publicstaticvoidmain(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类
// 用户信息新增实体类
publicclassUserBO{
    @EncryptField
    private String name;

    @EncryptField
    private String address;

    public String getName(){
        return name;
    }

    publicvoidsetName(String name){
        this.name = name;
    }

    public String getAddress(){
        return address;
    }

    publicvoidsetAddress(String address){
        this.address = address;
    }

    publicUserBO(String name, String address){
        this.name = name;
        this.address = address;
    }

    @Override
    public String toString(){
        return"UserBO{" +
                "name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }

    publicstaticvoidmain(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);
    }

    publicstatic Object encrypt(Object obj){
        return"加密: " + obj;
    }
}
AOP实战
切入点:
@RestController
@RequestMapping("/encrypt")
@Slf4j
publicclassEncryptController{

    @PostMapping("/v1")
    @Encryption// 切入点
    public UserBO insert(@RequestBody UserBO user){
        log.info("加密后对象:{}", user);
        return user;
    }
}
切面:
@Slf4j
@Aspect
@Component
publicclassEncryptAspect{

    //拦截需加密注解 切入点
    @Pointcut("@annotation(com.weige.javaskillpoint.annotation.Encryption)")
    publicvoidpoint(){

    }

    @Around("point()"//环绕通知
    public Object around(ProceedingJoinPoint joinPoint)throws Throwable {
        //加密逻辑处理
        encrypt(joinPoint);
        return joinPoint.proceed();
    }

}
为什么这里要使用AOP:无论是注解,反射,都需要一个启动方法,我上面演示的是通过main函数来启动。使用AOP,项目启动后,只要调用切入点对应的方法,就会根据切入点来形成一个切面,进行统一的逻辑增强;如果大家熟悉SpringMVC,SpringMVC提供了 ResponseBodyAdvice 和 RequestBodyAdvice两个接口,这两个接口可以对请求和响应进行预处理,就可以不需要使用AOP;
03
加密解密脱敏实战
项目目录:
图片
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
// 实体类
publicclassUserBO{
    @EncryptField
    private String name;

    @EncryptField
    private String address;

    public String getName(){
        return name;
    }

    publicvoidsetName(String name){
        this.name = name;
    }

    public String getAddress(){
        return address;
    }

    publicvoidsetAddress(String address){
        this.address = address;
    }

    publicUserBO(String name, String address){
        this.name = name;
        this.address = address;
    }

    @Override
    public String toString(){
        return"UserBO{" +
                "name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}
用户信息返回实体类 :UserDO
// 实体类
publicclassUserDO{

    @DecryptField(DesensitizationEnum.name)
    private String name;

    @DecryptField(DesensitizationEnum.address)
    private String address;

    public String getName(){
        return name;
    }

    publicvoidsetName(String name){
        this.name = name;
    }

    public String getAddress(){
        return address;
    }

    publicvoidsetAddress(String address){
        this.address = address;
    }

    publicUserDO(String name, String address){
        this.name = name;
        this.address = address;
    }
}
脱敏枚举
publicenum DesensitizationEnum {
    name,
    address,
    phone;
}
注解
解密字段注解(字段):
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public@interface DecryptField {
    DesensitizationEnum value();
}
解密方法注解(方法 作切入点):
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public@interface Decryption {

}
加密字段注解(字段):
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public@interface EncryptField {

}
加密方法注解(方法 作切入点):
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public@interface Encryption {

}
控制层
解密 Controller:
@RestController
@RequestMapping("/decrypt")
publicclassDecryptController{

    @GetMapping("/v1")
    @Decryption
    public UserDO decrypt(){
        returnnew UserDO("7c29e296e92893476db5f9477480ba7f""b5c7ff86ac36c01dda45d9ffb0bf73194b083937349c3901f571d42acdaa7bae");
    }

}
加密 Controller:
@RestController
@RequestMapping("/encrypt")
@Slf4j
publicclassEncryptController{

    @PostMapping("/v1")
    @Encryption
    public UserBO insert(@RequestBody UserBO user){
        log.info("加密后对象:{}", user);
        return user;
    }
}
切面
解密脱敏切面:
@Slf4j
@Aspect
@Component
publicclassDecryptAspect{
    //拦截需解密注解
    @Pointcut("@annotation(com.weige.javaskillpoint.annotation.Decryption)")
    publicvoidpoint(){

    }

    @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)) {
            returnnull;
        }
        if (obj instanceof ArrayList) {
            decryptList(obj);
        } else {
            decryptObj(obj);
        }
        return obj;
    }

    privatevoiddecryptObj(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);
                }
            }
        }
    }

    privatevoiddecryptList(Object obj)throws IllegalAccessException {
        List<Object> result = new ArrayList<>();
        if (obj instanceof ArrayList) {
            result.addAll((Collection<?>) obj);
        }
        for (Object object : result) {
            decryptObj(object);
        }
    }

    privatevoiddecryptValue(){
        log.info("根据对象进行解密脱敏,单个字段不做处理!");
    }
}
加密切面:
@Slf4j
@Aspect
@Component
publicclassEncryptAspect{

    //拦截需加密注解
    @Pointcut("@annotation(com.weige.javaskillpoint.annotation.Encryption)")
    publicvoidpoint(){

    }

    @Around("point()")
    public Object around(ProceedingJoinPoint joinPoint)throws Throwable {
        //加密
        encrypt(joinPoint);
        return joinPoint.proceed();
    }

    publicvoidencrypt(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
publicclassAesUtil{

    // 默认16位 或 128 256位
    publicstatic String AES_KEY = "Wk#qerdfdshbd910";

    publicstatic AES aes = SecureUtil.aes(AES_KEY.getBytes());

    publicstatic Object encrypt(Object obj){
        return aes.encryptHex((String) obj);
    }

    publicstatic Object decrypt(Object obj, DesensitizationEnum desensitizationEnum){
        // 解密
        Object decrypt = decrypt(obj);
        // 脱敏
        return DesensitizationUtil.desensitization(decrypt, desensitizationEnum);
    }

    publicstatic Object decrypt(Object obj){
        return aes.decryptStr((String) obj, CharsetUtil.CHARSET_UTF_8);
    }

}
脱敏工具类:DesensitizationUtil
publicclassDesensitizationUtil{


    publicstatic 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开始
     */

    publicstatic Object strUtilHide(String obj, int start, int end){
        return StrUtil.hide(obj, start, end);
    }

    publicstatic Object strUtilHide(Object obj, int start){
        return strUtilHide(((String) obj), start, ((String) obj).length());
    }

}
完结
以上代码不难,大伙复制到本地跑一遍,基本就能理解;愿每一位程序员少走弯路!
来源:blog.csdn.net/qq_43372633/article/details/132055143
END
PS:防止找不到本篇文章,可以收藏点赞,方便翻阅查找哦。



往期推荐



蚂蚁又开源了一个顶级 Java 项目!

网易二面:阿里为何建议MVC+Manager层混合架构?

从一个程序员的角度告诉你:“12306”有多牛逼!

Arthas全面使用指南:离线安装+Docker/K8s集成+集中管理

如何优雅实现多账号统一登录?so easy!

HTTPS 行为大赏:三分钟了解加密过程


【声明】内容源于网络
0
0
终码一生
开发者聚集地。分享Java相关开发技术(JVM,多线程,高并发,性能调优等),开源项目,常见开发问题和前沿科技资讯!
内容 1876
粉丝 0
终码一生 开发者聚集地。分享Java相关开发技术(JVM,多线程,高并发,性能调优等),开源项目,常见开发问题和前沿科技资讯!
总阅读115
粉丝0
内容1.9k