当前位置: 代码网 > it编程>编程语言>Java > SpringBoot+Mybatis通过自定义注解实现字段加密存储方式

SpringBoot+Mybatis通过自定义注解实现字段加密存储方式

2026年03月02日 Java 我要评论
前言通过mybatis提供的拦截器,在新增、修改时对特定的敏感字段进行加密存储,查询时自动进行解密操作,减少业务层面的代码逻辑;加密存储意义:防止数据泄露:即使数据库被非法访问或泄露,加密数据也无法被

前言

通过mybatis提供的拦截器,在新增、修改时对特定的敏感字段进行加密存储,查询时自动进行解密操作,减少业务层面的代码逻辑;

加密存储意义:

  • 防止数据泄露:即使数据库被非法访问或泄露,加密数据也无法被直接利用
  • 保护个人隐私:如身份证号、手机号、住址等pii(个人身份信息)数据
  • 保障财务安全:加密银行卡号、支付密码等金融信息

核心逻辑:

  • 自定义注解,对需要进行加密存储的使用注解进行标注;
  • 构建aes对称加密工具类;
  • 实现mybatis拦截器,通过反射获取当前实体类的字段是否需要进行加解密;

实现

自定义注解

通过自定义@encryptdbbean@encryptdbcolumn标识某个do实体类的某些字段需要进行加解密处理;

  • encryptdbbean:作用在类上
  • encryptdbcolumn:作用在字段上
@inherited
@target({elementtype.type})
@retention(retentionpolicy.runtime)
public @interface encryptdbbean {
}
@retention(retentionpolicy.runtime)
@target(elementtype.field)
public @interface encryptdbcolumn {
}

aes对称加密工具类

import javax.crypto.cipher;
import javax.crypto.spec.ivparameterspec;
import javax.crypto.spec.secretkeyspec;
import java.util.base64;

public class dbaesutils {
    /**
     * 设置为cbc加密模式,默认情况下ecb比cbc更高效
     */
    private final static string cbc = "/cbc/pkcs5padding";
    private final static string algorithm = "aes";

    /**
     * 定义密钥key,aes加密算法,key的大小必须是16个字节
     */
    private final static string key = "1234567812345678";

    /**
     * 设置偏移量,iv值任意16个字节
     */
    private final static string iv = "1122334455667788";

    /**
     * 对称加密数据
     *
     * @return : 密文
     * @throws exception
     */
    public static string encryptbysymmetry(string input) {
        try {
            // cbc模式
            string transformation = algorithm + cbc;
            // 获取加密对象
            cipher cipher = cipher.getinstance(transformation);
            // 创建加密规则
            // 第一个参数key的字节
            // 第二个参数表示加密算法
            secretkeyspec sks = new secretkeyspec(key.getbytes(), algorithm);

            // encrypt_mode:加密模式
            // decrypt_mode: 解密模式
            // 使用cbc模式
            ivparameterspec iv = new ivparameterspec(iv.getbytes());
            cipher.init(cipher.encrypt_mode, sks, iv);


            // 加密
            byte[] bytes = cipher.dofinal(input.getbytes());

            // 输出加密后的数据
            return base64.getencoder().encodetostring(bytes);
        } catch (exception e) {
            throw new runtimeexception("加密失败!", e);
        }
    }

    /**
     * 对称解密
     *
     * @param input : 密文
     * @throws exception
     * @return: 原文
     */
    public static string decryptbysymmetry(string input) {
        try {
            // cbc模式
            string transformation = algorithm + cbc;

            // 1,获取cipher对象
            cipher cipher = cipher.getinstance(transformation);
            // 指定密钥规则
            secretkeyspec sks = new secretkeyspec(key.getbytes(), algorithm);

            // 使用cbc模式
            ivparameterspec iv = new ivparameterspec(iv.getbytes());
            cipher.init(cipher.decrypt_mode, sks, iv);

            // 3. 解密,上面使用的base64编码,下面直接用密文
            byte[] bytes = cipher.dofinal(base64.getdecoder().decode(input));
            //  因为是明文,所以直接返回
            return new string(bytes);
        } catch (exception e) {
            throw new runtimeexception("解密失败!", e);
        }
    }
}

创建拦截器

  • 加密拦截器:encryptinterceptor
  • 解密拦截器:decryptinterceptor

加密拦截器

在新增或者更新时,通过拦截对被注解标识的字段进行加密存储处理;

import com.lhz.demo.annotation.encryptdbbean;
import com.lhz.demo.annotation.encryptdbcolumn;
import com.lhz.demo.utils.dbaesutils;
import lombok.extern.slf4j.slf4j;
import org.apache.ibatis.executor.parameter.parameterhandler;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.component;

import java.lang.reflect.field;
import java.sql.preparedstatement;
import java.util.*;

@slf4j
@component
@intercepts({
        @signature(type = parameterhandler.class, method = "setparameters", args = {preparedstatement.class}),
})
public class encryptinterceptor implements interceptor {

    @override
    public object intercept(invocation invocation) throws throwable {
        try {
            parameterhandler parameterhandler = (parameterhandler) invocation.gettarget();
            field parameterfield = parameterhandler.getclass().getdeclaredfield("parameterobject");
            parameterfield.setaccessible(true);
            object parameterobject = parameterfield.get(parameterhandler);

            if (parameterobject != null) {
                set<object> objectlist = new hashset<>();
                if (parameterobject instanceof map<?, ?>) {
                    collection<?> values = ((map<?, ?>) parameterobject).values();
                    objectlist.addall(values);
                } else {
                    objectlist.add(parameterobject);
                }
                for (object o1 : objectlist) {
                    class<?> o1class = o1.getclass();
                    // 实体类是否存在 加密注解
                    boolean encryptdbbean = o1class.isannotationpresent(encryptdbbean.class);
                    if (encryptdbbean) {
                        //取出当前当前类所有字段,传入加密方法
                        field[] declaredfields = o1class.getdeclaredfields();
                        // 便利字段,是否存在加密注解,并且进行加密处理
                        for (field field : declaredfields) {
                            //取出所有被encryptdecryptfield注解的字段
                            boolean annotationpresent = field.isannotationpresent(encryptdbcolumn.class);
                            if (annotationpresent) {
                                field.setaccessible(true);
                                object object = field.get(o1);
                                if (object != null) {
                                    string value = object.tostring();
                                    //加密  这里我使用自定义的aes加密工具
                                    field.set(o1, dbaesutils.encryptbysymmetry(value));
                                }
                            }
                        }
                    }
                }
            }
            return invocation.proceed();
        } catch (exception e) {
            throw new runtimeexception("字段加密失败!", e);
        }
    }

    /**
     * 默认配置,否则当前拦截器不会加入拦截器链
     */
    @override
    public object plugin(object o) {
        return plugin.wrap(o, this);
    }

}

解密拦截器

将查询的数据,返回为do实体类时,对被注解标识的字段进行解密处理

import com.lhz.demo.annotation.encryptdbbean;
import com.lhz.demo.annotation.encryptdbcolumn;
import com.lhz.demo.utils.dbaesutils;
import lombok.extern.slf4j.slf4j;
import org.apache.ibatis.executor.resultset.resultsethandler;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.component;
import org.springframework.util.collectionutils;

import java.lang.reflect.field;
import java.sql.statement;
import java.util.arraylist;
import java.util.list;
import java.util.objects;

@intercepts({@signature(type = resultsethandler.class, method = "handleresultsets", args = {statement.class})})
@slf4j
@component
public class decryptinterceptor implements interceptor {
    @override
    public object intercept(invocation invocation) throws throwable {
        object resultobject = invocation.proceed();
        try {
            if (objects.isnull(resultobject)) {
                return null;
            }
            // 查询列表数据
            if (resultobject instanceof arraylist) {
                list list = (arraylist) resultobject;
                if (!collectionutils.isempty(list)) {
                    for (object result : list) {
                        class<?> objectclass = result.getclass();
                        boolean encryptdbbean = objectclass.isannotationpresent(encryptdbbean.class);
                        if (encryptdbbean) {
                            // 解密处理
                            decrypt(result);
                        }
                    }
                }
            } else {
                // 查询单个数据
                class<?> objectclass = resultobject.getclass();
                boolean encryptdbbean = objectclass.isannotationpresent(encryptdbbean.class);
                if (encryptdbbean) {
                    // 解密处理
                    decrypt(resultobject);
                }
            }
            return resultobject;
        } catch (exception e) {
            throw new runtimeexception("字段解密失败!", e);
        }

    }

    @override
    public object plugin(object o) {
        return plugin.wrap(o, this);
    }

    public <t> void decrypt(t result) throws exception {
        //取出resulttype的类
        class<?> resultclass = result.getclass();
        field[] declaredfields = resultclass.getdeclaredfields();
        for (field field : declaredfields) {
            boolean annotationpresent = field.isannotationpresent(encryptdbcolumn.class);
            if (annotationpresent) {
                field.setaccessible(true);
                object object = field.get(result);
                if (object != null) {
                    string value = object.tostring();
                    //对注解的字段进行逐一解密
                    field.set(result, dbaesutils.decryptbysymmetry(value));
                }
            }
        }
    }
}

验证

创建实体类

创建实体类,并且使用加密注解@encryptdbbean@encryptdbcolumn进行标注,此处以手机号为例;

@data
@tablename("sys_user_info")
@encryptdbbean
public class testentity {
    /**
     * 用户id
     */
    @tableid("id")
    private long id;

    /**
     * 用户名称
     */
    private string name;

    /**
     * 手机号
     */
    @encryptdbcolumn
    private string mobile;
}

数据写入与查询

对数据的操作使用伪代码进行表示

testentity entity = new testentity();
entity.setid(1l);
entity.setname("测试");
entity.setmobile("166xxxx8888");
// 插入数据
entityservice.insert(entity);
// 更新数据
entity.setmobile("166xxxx7777");
entityservice.updatebyid(entity);


// 列表查询
list<testentity> list = testservice.list();

效果:

  • insert和update后的数据,在数据库是加密字符串存储的形式;
  • list方法查询的数据,将明文进行显示;

加密字段参与查询

如果是加密字段进行条件查询时,需要自行将查询参数进行加密处理,因为数据库是存储的密文,所以查询时也需要使用密文进行匹配,比如:要查询mobile=111的数据

// 伪代码
// 获取前端传入的查询条件
string mobile = "111"
// 手动加密
mobile = dbaesutils.decryptbysymmetry(mobile );
testservice.selectbymobile(mobile);

不生效情况

1、在通过lambdaquerywrapper获取querywrapper方式查询时,拦截器无法获取自定义注解对象,需要手动对查询的字段进行加密,比如:

如果是 通过自定义的xml查询,如果入参有加密注解,那么会自动对字段进行加密处理 testmapper.listtest(testentity)

lambdaquerywrapper<testentity> wrapper = new lambdaquerywrapper<>();
string mobile = test.getmobile();
if (mobile != null) {
   // mobile在数据库中加密储存,此处需要手动进行加密
   mobile = dbaesutils.encryptbysymmetry(mobile);
}
wrapper.eq(stringutils.isnotblank(test.getmobile()), testentity::getmobile, mobile);
list<testentity> testentities = testmapper.selectlist(wrapper);

2、使用mybatis提供的selectone或者getone方法查询时,无法对响应的数据进行解密,需要手动进行处理,比如:

如果是 通过自定义的xml查询,无论多少条数据都会对数据进行解密,testmapper.selectxmlbyid(long id)

testentity one = testservice.getone(new querywrapper<>(), false);
// mobile在数据库中加密储存,此处需要手动进行解密
one.setmobile(dbaesutils.decryptbysymmetry(one.getmobile()));

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com