【Mybatis】基于Mybatis插件+注解,实现敏感数据自动加解密

news2025/4/25 8:19:47

一、介绍

业务场景中经常会遇到诸如用户手机号,身份证号,银行卡号,邮箱,地址,密码等等信息,属于敏感信息,需要保存在数据库中。而很多公司会会要求对数据库中的此类数据进行加密存储。

敏感数据脱敏需要处理的两个问题:

  1. 查询操作,需要对查询的关键字进行加密,同时也要对从库中查到的数据进行解密
  2. 插入和更新操作,需要对插入或者更新的数据进行加密,然后保存到数据库。

二、解决思路

  1. 最简单的方式:对代码中涉及的敏感数据接口在查询和插入、更新时进行加解密。缺点是工作量大,代码侵入多
  2. 在mybatis中进行统一拦截,上层业务调用不需要再考虑敏感数据的加解密问题。

通过查阅资料,发现思路二目前普遍有两种处理方式:

  1. 采用mybatis插件在mybatis SQL执行和查询结果填充操作上进行切入
  2. 使用mybatis框架提供的TypeHandler来实现在持久层处理数据

本文介绍第一种方式,即使用Mybatis的插件,通过拦截器实现敏感数据加解密

三、mybatis插件原理

Mybatis的插件,是采用责任链机制,通过JDK动态代理来实现的。默认情况下,Mybatis允许使用插件来拦截四个对象:

  • Executor:执行CURD操作;
  • StatementHandler:处理sql语句预编译,设置参数等相关工作;
  • ParameterHandler:设置预编译参数用的;
  • ResultSetHandler:处理结果集。
    在这里插入图片描述
    编写插件需要标识拦截方法和实现拦截逻辑。
    标识拦截拦截方法是通过注解org.apache.ibatis.plugin.Intercepts和注解org.apache.ibatis.plugin.Signature实现的。

四、实现

  1. 设置参数时对参数中含有敏感字段的数据进行加密
  2. 对查询返回的结果进行解密处理
    基于上面两种要求,我们只需要对ParameterHandlerResultSetHandler进行切入。

定义特定注解,在切入时只需要检查字段中是否包含该注解来决定是否加解密

加解密注解

包含两个注解:

  1. SensitiveData注解:用在实体类上,表示此类有些字段需要加密,需要结合@SensitiveField一起使用
  2. SensitiveField注解:用在类的字段上或者方法的参数上,表示该字段或参数需要加密
1. 定义SensitiveData注解
package com.zsx.annotation;

import java.lang.annotation.*;
/**
 * 该注解定义在类上
 * 插件通过扫描类对象是否包含这个注解来决定是否继续扫描其中的字段注解
 * 这个注解要配合SensitiveField注解
 * @author zhousx
 * @create 2023/10/01-22:45
 **/
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {

}
2. 定义SensitiveField注解
package com.zsx.annotation;

import java.lang.annotation.*;
/**
 * 该注解有两种使用方式
 * ①:配合@SensitiveData加在类中的字段上
 * ②:直接在Mapper中的方法参数上使用
 * @author zhousx
 * @create 2023/10/01-22:45
 **/
@Documented
@Inherited
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveField {

}

插件实现

1. 参数插件ParameterInterceptor

切入mybatis设置参数时对敏感数据进行加密
Mybatis插件的使用就是通过实现Mybatis中的Interceptor接口
再配合@Intercepts注解

// 使用mybatis插件时需要定义签名
// type标识需要切入的Handler
// method表示要要切入的方法
// args表示要切入的方法的参数
@Intercepts({
@Signature(type = ParameterHandler.class, method = “setParameters”, args = PreparedStatement.class),
})

上面这个签名就表示:切入ParameterHandler类的setParameters(PreparedStatement preparedStatement)方法

ParameterInterceptor .java

package com.zsx.intercepter;

import com.baomidou.mybatisplus.core.MybatisParameterHandler;
import com.zsx.annotation.SensitiveField;
import com.zsx.annotation.SensitiveData;
import com.zsx.utils.DBAESUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.sql.PreparedStatement;
import java.util.*;

/**
 * @author zhousx
 * @create 2023/10/01-22:45
 **/
@Slf4j
// 注入Spring
@Component
@Intercepts({
        @Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
})
public class ParameterInterceptor implements Interceptor {

    @Autowired
    private com.zsx.utils.IEncryptUtil IEncryptUtil;


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //@Signature 指定了 type= parameterHandler 后,这里的 invocation.getTarget() 便是parameterHandler
        //若指定ResultSetHandler ,这里则能强转为ResultSetHandler
        MybatisParameterHandler parameterHandler = (MybatisParameterHandler) invocation.getTarget();
        // 获取参数对像,即 mapper 中 paramsType 的实例
        Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");
        parameterField.setAccessible(true);
        //取出实例
        Object parameterObject = parameterField.get(parameterHandler);
        // 搜索该方法中是否有需要加密的普通字段
        List<String> paramNames = searchParamAnnotation(parameterHandler);
        if (parameterObject != null) {
            Class<?> parameterObjectClass = parameterObject.getClass();
            //对类字段进行加密
            //校验该实例的类是否被@SensitiveData所注解
            SensitiveData sensitiveData = AnnotationUtils.findAnnotation(parameterObjectClass, SensitiveData.class);
            if (Objects.nonNull(sensitiveData)) {
                //取出当前当前类所有字段,传入加密方法
                Field[] declaredFields = parameterObjectClass.getDeclaredFields();
                IEncryptUtil.encrypt(declaredFields, parameterObject);
            }

            //如果传参为List类型,对list里的对象进行加密
            processListParam(parameterObject);

            // 对普通字段进行加密
            if (!CollectionUtils.isEmpty(paramNames)) {
                // 反射获取 BoundSql 对象,此对象包含生成的sql和sql的参数map映射
                Field boundSqlField = parameterHandler.getClass().getDeclaredField("boundSql");
                boundSqlField.setAccessible(true);
                BoundSql boundSql = (BoundSql) boundSqlField.get(parameterHandler);
                PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0];
                // 改写参数
                processParam(parameterObject, paramNames);
                // 改写的参数设置到原parameterHandler对象
                parameterField.set(parameterHandler, parameterObject);
                parameterHandler.setParameters(ps);
            }
        }
        return invocation.proceed();
    }

    /**
     * 如果传参为List类型,对list里的对象判断,是否进行加密
     * @param parameterObject
     * @throws IllegalAccessException
     */
    private void processListParam(Object parameterObject) throws IllegalAccessException {
        // mybatis会把list封装到一个ParamMap中
        if (parameterObject instanceof Map) {
            //1. 如果不对传参users使用@Param注解,则会在map中放入collection、list、users三个键值对,这三个指向同一个内存地址即内容相同。
            if (((Map) parameterObject).containsKey("list")) {
                Map map = (Map) parameterObject;
                ArrayList list = (ArrayList) map.get("list");
                Object element = list.get(0);
                Class<?> elementClass = element.getClass();
                SensitiveData tempSensitiveData = AnnotationUtils.findAnnotation(elementClass, SensitiveData.class);
                if (Objects.nonNull(tempSensitiveData)) {
                    for (Object elementObject : list) {
                        Field[] declaredFields = elementClass.getDeclaredFields();
                        IEncryptUtil.encrypt(declaredFields, elementObject);
                    }
                }
            }

            //2. 如果使用了@Param注解对参数重命名为users,那么map中只会放入users、param1两个键值对,这2个指向同一个内存地址即内容相同。
            if (((Map) parameterObject).containsKey("param1")) {
                Map map = (Map) parameterObject;
                Object param1 = map.get("param1");
                //如果param1是ArrayList,则转为arrayList。否则不转换
                if (param1 instanceof ArrayList) {
                    ArrayList list = (ArrayList) param1;
                    Object element = list.get(0);
                    Class<?> elementClass = element.getClass();
                    SensitiveData tempSensitiveData = AnnotationUtils.findAnnotation(elementClass, SensitiveData.class);
                    if (Objects.nonNull(tempSensitiveData)) {
                        for (Object elementObject : list) {
                            Field[] declaredFields = elementClass.getDeclaredFields();
                            IEncryptUtil.encrypt(declaredFields, elementObject);
                        }
                    }
                }
            }
        }
    }


    /**
     * 处理普通参数,对params中的String参数进行加密
     * @param parameterObject
     * @param params
     * @throws Exception
     */
    private void processParam(Object parameterObject, List<String> params) throws Exception {
        // 处理参数对象  如果是 map 且map的key 中没有 tenantId,添加到参数map中
        // 如果参数是bean,反射设置值

        if (parameterObject instanceof Map) {
            @SuppressWarnings("unchecked")
            Map<String, String> map = ((Map<String, String>) parameterObject);
            for (String param : params) {
                String value = map.get(param);
                map.put(param, value == null ? null : DBAESUtil.encrypt(value));
            }
//            parameterObject = map;
        }
    }

    /**
     * 查找参数的注解是否是含有 @EncryptTransaction注解,如果是,则存储参数名
     * @param parameterHandler
     * @return
     * @throws NoSuchFieldException
     * @throws ClassNotFoundException
     * @throws IllegalAccessException
     */
    private List<String> searchParamAnnotation(ParameterHandler parameterHandler) throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException {
        Class<MybatisParameterHandler> handlerClass = MybatisParameterHandler.class;
        Field mappedStatementFiled = handlerClass.getDeclaredField("mappedStatement");
        mappedStatementFiled.setAccessible(true);
        MappedStatement mappedStatement = (MappedStatement) mappedStatementFiled.get(parameterHandler);
        String methodName = mappedStatement.getId();
        Class<?> mapperClass = Class.forName(methodName.substring(0, methodName.lastIndexOf('.')));
        methodName = methodName.substring(methodName.lastIndexOf('.') + 1);
        Method[] methods = mapperClass.getDeclaredMethods();
        Method method = null;
        for (Method m : methods) {
            if (m.getName().equals(methodName)) {
                method = m;
                break;
            }
        }
        List<String> paramNames = null;
        if (method != null) {

            Annotation[][] pa = method.getParameterAnnotations();
            Parameter[] parameters = method.getParameters();
            for (int i = 0; i < pa.length; i++) {
                for (Annotation annotation : pa[i]) {
                    if (annotation instanceof SensitiveField) {
                        if (paramNames == null) {
                            paramNames = new ArrayList<>();
                        }
                        paramNames.add(parameters[i].getName());
                    }
                }
            }
        }
        return paramNames;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}


2. 返回值插件ResultSetInterceptor

ResultSetInterceptor .java

package com.zsx.intercepter;

import com.zsx.annotation.SensitiveData;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.sql.Statement;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Properties;

/**
 * @author zhousx
 * @create 2023/10/01-22:45
 **/
@Slf4j
@Component
@Intercepts({
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class ResultSetInterceptor implements Interceptor {

    @Autowired
    private com.zsx.utils.IDecryptUtil IDecryptUtil;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //取出查询的结果
        Object resultObject = invocation.proceed();
        if (Objects.isNull(resultObject)) {
            return null;
        }
        //基于selectList
        if (resultObject instanceof ArrayList) {
            @SuppressWarnings("unchecked")
            ArrayList<Objects> resultList = (ArrayList<Objects>) resultObject;
            if (!CollectionUtils.isEmpty(resultList) && needToDecrypt(resultList.get(0))) {
                for (Object result : resultList) {
                    //逐一解密
                    IDecryptUtil.decrypt(result);
                }
            }
            //基于selectOne
        } else {
            if (needToDecrypt(resultObject)) {
                IDecryptUtil.decrypt(resultObject);
            }
        }
        return resultObject;
    }


    private boolean needToDecrypt(Object object) {
        Class<?> objectClass = object.getClass();
        SensitiveData sensitiveData = AnnotationUtils.findAnnotation(objectClass, SensitiveData.class);
        return Objects.nonNull(sensitiveData);
    }


    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}


加解密工具类

1. 加密接口

IEncryptUtil .java

package com.zsx.utils;

import java.lang.reflect.Field;

/**
 * @author zhousx
 * @create 2023/10/01-22:45
 **/
public interface IEncryptUtil {
    /**
     * 加密
     *
     * @param declaredFields 加密字段
     * @param paramsObject   对象
     * @param <T>            入参类型
     * @return 返回加密
     * @throws IllegalAccessException 不可访问
     */
    <T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException;
}
2. 解密接口

IDecryptUtil .java

package com.zsx.utils;

/**
 * @author zhousx
 * @create 2023/10/01-22:45
 **/
public interface IDecryptUtil {
    /**
     * 解密
     *
     * @param result resultType的实例
     * @return T
     * @throws IllegalAccessException 字段不可访问异常
     */
    <T> T decrypt(T result) throws IllegalAccessException;
}
3. 加密实现类

EncryptImpl .java

package com.zsx.utils;

import com.zsx.annotation.SensitiveField;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.Objects;

/**
 * @author zhousx
 * @create 2023/10/01-22:45
 **/
@Component
public class EncryptImpl implements IEncryptUtil {

    @Override
    public <T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException {
            //取出所有被EncryptTransaction注解的字段
        for (Field field : declaredFields) {
            SensitiveField sensitiveField = field.getAnnotation(SensitiveField.class);
            if (!Objects.isNull(sensitiveField)) {
                field.setAccessible(true);
                Object object = field.get(paramsObject);
                //暂时只实现String类型的加密
                if (object instanceof String) {
                    String value = (String) object;

                    //修改: 如果有标识则不加密,没有则加密并加上标识前缀。(防止重复加密)
                    String encrypt = value;

                    //开始对字段加密使用自定义的AES加密工具
                    try {
                        if(!value.startsWith(DBAESUtil.KEY_SENSITIVE)) {
                            encrypt = DBAESUtil.encrypt(value);
                            encrypt = DBAESUtil.KEY_SENSITIVE + encrypt;
                        }
                        //修改字段值
                        field.set(paramsObject, encrypt);
                        
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return paramsObject;
    }
}



4. 解密实现类

DecryptImpl.java

package com.zsx.utils;

import com.zsx.annotation.SensitiveField;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.Objects;

/**
 * @author zhousx
 * @create 2023/10/01-22:45
 **/
@Component
public class DecryptImpl implements IDecryptUtil {

    /**
     * 解密
     *
     * @param result resultType的实例
     */
    @Override
    public <T> T decrypt(T result) throws IllegalAccessException {
        //取出resultType的类
        Class<?> resultClass = result.getClass();
        Field[] declaredFields = resultClass.getDeclaredFields();
        for (Field field : declaredFields) {
            //取出所有被DecryptTransaction注解的字段
            SensitiveField sensitiveField = field.getAnnotation(SensitiveField.class);
            if (!Objects.isNull(sensitiveField)) {
                field.setAccessible(true);
                Object object = field.get(result);
                //String的解密
                if (object instanceof String) {
                    String value = (String) object;
                    //对注解的字段进行逐一解密
                    try {
                        //修改:没有标识则不解密(防止重复解密)
                        if(value.startsWith(DBAESUtil.KEY_SENSITIVE)) {
                            value = value.substring(10);
                            value = DBAESUtil.decrypt(value);
                        }
                        //修改字段值
                        field.set(result, value);

                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return result;
    }
}

5. 加解密工具类
package com.zsx.utils;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;


/**
 * @author zhousx
 * @create 2023/10/01-22:45
 **/
public class DBAESUtil {
    /**
     * 加密标识:字符串有这个前缀就说明已经加密过
     */
    public static final String KEY_SENSITIVE = "sensitive_";
    
    private static final String DEFAULT_V = "6859505890402435";
    // 自己填写
    private static final String KEY = "***";
    private static final String ALGORITHM = "AES";

    private static SecretKeySpec getKey() {
        byte[] arrBTmp = DBAESUtil.KEY.getBytes();
        // 创建一个空的16位字节数组(默认值为0)
        byte[] arrB = new byte[16];
        for (int i = 0; i < arrBTmp.length && i < arrB.length; i++) {
            arrB[i] = arrBTmp[i];
        }
        return new SecretKeySpec(arrB, ALGORITHM);
    }

    /**
     * 加密
     */
    public static String encrypt(String content) throws Exception {
        final Base64.Encoder encoder = Base64.getEncoder();
        SecretKeySpec keySpec = getKey();
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        IvParameterSpec iv = new IvParameterSpec(DEFAULT_V.getBytes());
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
        byte[] encrypted = cipher.doFinal(content.getBytes());
        return encoder.encodeToString(encrypted);
    }

    /**
     * 解密
     */
    public static String decrypt(String content) throws Exception {
        final Base64.Decoder decoder = Base64.getDecoder();
        SecretKeySpec keySpec = getKey();
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        IvParameterSpec iv = new IvParameterSpec(DEFAULT_V.getBytes());
        cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
        byte[] base64 = decoder.decode(content);
        byte[] original = cipher.doFinal(base64);
        return new String(original);
    }

}

使用

1. 注解在实体类上
package com.zsx.entity;

import com.zsx.annotation.SensitiveField;
import com.zsx.annotation.SensitiveData;
import lombok.*;
import java.io.Serializable;


/**
 * @author zhousx
 */
@With
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
@SensitiveData // 插件只对加了该注解的类进行扫描,只有加了这个注解的类才会生效
public class User implements Serializable {
    private Integer id;
    private String name;

    // 表明对该字段进行加密
    @SensitiveField
    private String email;
    // 表明对该字段进行加密
    @SensitiveField
    private String phone;

}
2. 注解在参数上
package com.zsx.mapper;



import com.zsx.annotation.SensitiveField;
import com.zsx.annotation.SensitiveData;
import com.zsx.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface IUserDao {

    /**
     * 测试查询 普通参数加密
     * @param phone
     * @return
     */
    List<User> getUserByPhone(@SensitiveField@Param("phone") String phone);
    
    /**
     * 测试插入 普通参数加密,多个需要加密的字段
     * @param name
     * @param email
     * @param phone
     * @return
     */
    int insertUserByParam(@Param("name") String name, @SensitiveField@Param("email") String email, @SensitiveField@Param("phone") String phone);
}

完整测试用例

UserController.java

/**
 * Project Name: test-zsx
 * File Name: UserController
 * Package Name: com.zsx.controller
 * Date: 2023/9/13 11:21
 * Copyright (c) 2023 天翼数字生活科技有限公司 All Rights Reserved.
 */
package com.zsx.controller;

import com.alibaba.fastjson2.JSON;
import com.zsx.entity.User;
import com.zsx.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
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;

import java.util.List;

/**
 * @description: 测试mybatis拦截器+注解 实现数据的自动加解密功能
 * @author: zhoushaoxiong
 * @date: 2023/9/13 11:21
 */
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private IUserService userService;

    /**
     * 插入一条记录
     * @param user
     * @return
     */
    @PostMapping("/add")
    public String addUser(@RequestBody User user){
        userService.addUser(user);
        return "success";
    }

    /**
     * 查询一条记录
     * @param id
     * @return
     */
    @PostMapping("/get/one")
    public String getUser(Long id){
        User user = userService.getUserById(id);
        return user.toString();
    }

    /**
     * 查询全部
     * @return
     */
    @PostMapping("/get/list")
    public String getUserAll(){
        List<User> users = userService.getAllUser();
        return JSON.toJSONString(users);
    }

    /**
     * 通过手机号查询
     * @param phone
     * @return
     */
    @PostMapping("/get/phone")
    public String getUserByPhone(String phone){
        List<User> users = userService.getUserByPhone(phone);
        return JSON.toJSONString(users);
    }

    /**
     * 通过对象查询
     * @param phone
     * @return
     */
    @PostMapping("/get/user/phone")
    public String getUserByUserPhone(String phone){
        List<User> users = userService.getUser(phone);
        return JSON.toJSONString(users);
    }

    /**
     * 批量插入
     * @return
     */
    @PostMapping("/add/list")
    public String addUserList(){
        List<User> users = userService.addUserList();
        return JSON.toJSONString(users);
    }

    /**
     * 插入 dao使用@Param注解
     * @return
     */
    @PostMapping("/add/user/param")
    public String addUserParam(){
        int result = userService.addUserByParam();
        return JSON.toJSONString(result);
    }
}

IUserDao.java

package com.zsx.mapper;



import com.zsx.annotation.SensitiveField;
import com.zsx.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface IUserDao {

    /**
     * 测试查询 普通参数 拦截器对结果对象进行解密
     * @param id
     * @return
     */
    User getUserById(Long id);

    /**
     * 测试插入 传参为对象,拦截器对含有加密注解的对象的属性 进行自动加密
     * @param user
     * @return
     */
    int addUser(User user);

    /**
     * 测试查询 查询结果为list 需要对list结果里的对象属性进行解密
     * @return
     */
    List<User> getAllUsers();

    /**
     * 测试查询 普通参数加密
     * @param phone
     * @return
     */
    List<User> getUserByPhone(@SensitiveField @Param("phone") String phone);

    /**
     * 测试查询 传参为对象 对象中的phone参数需要拦截器进行加密才能查询
     * @param user
     * @return
     */
    List<User> getUserByUser(User user);


    /**
     * 测试插入 对list进行加密
     * @param users
     * @return
     */
    int insertBatch(List<User> users);

    /**
     * 测试插入 使用@Param注解 对list进行加密
     * @param users
     * @return
     */
    int insertBatchByParam(@Param("users") List<User> users);

    /**
     * 测试插入 普通参数加密,多个需要加密的字段
     * @param name
     * @param email
     * @param phone
     * @return
     */
    int insertUserByParam(@Param("name") String name, @SensitiveField @Param("email") String email, @SensitiveField @Param("phone") String phone);
}

UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zsx.mapper.IUserDao">

    <select id="getUserById" resultType="com.zsx.entity.User">
        SELECT * FROM user WHERE id = #{id}
    </select>

    <select id="getAllUsers" resultType="com.zsx.entity.User">
        select * from user
    </select>

    <select id="getUserByPhone" resultType="com.zsx.entity.User">
        select * from user where phone = #{phone}
    </select>

    <select id="getUserByUser" parameterType="com.zsx.entity.User" resultType="com.zsx.entity.User">
        select * from user where phone = #{phone}
    </select>

    <insert id="addUser" parameterType="com.zsx.entity.User">
        insert into user (name, email, phone) values (#{name}, #{email}, #{phone})
    </insert>

    <insert id="insertBatch" parameterType="com.zsx.entity.User">
        insert into user (name, email, phone) values
        <foreach collection="list"  separator="," item="item">
            (#{item.name}, #{item.email}, #{item.phone})
        </foreach>
    </insert>

    <insert id="insertBatchByParam" parameterType="com.zsx.entity.User">
        insert into user (name, email, phone) values
        <foreach collection="users"  separator="," item="item">
            (#{item.name}, #{item.email}, #{item.phone})
        </foreach>
    </insert>

    <insert id="insertUserByParam">
        insert into user (name, email, phone) values (#{name}, #{email}, #{phone})
    </insert>


</mapper>

引入依赖

    <dependencies>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>


        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.26</version>
        </dependency>
    </dependencies>

需要注意的点

1. dao层的传参为List

需要对List中的对象元素进行判断.如果对象是需要加密的,则List元素要逐一加密处理。

比如dao层方法:

    int insertBatch(List<User> users);

因此需要对List元素进行判断和处理:

    //如果传参为List类型,对list里的对象进行加密
    processListParam(parameterObject);
    
    /**
     * 如果传参为List类型,对list里的对象判断,是否进行加密
     * @param parameterObject
     * @throws IllegalAccessException
     */
    private void processListParam(Object parameterObject) throws IllegalAccessException {
        // mybatis会把list封装到一个ParamMap中
        if (parameterObject instanceof Map) {
            //1. 如果不对传参users使用@Param注解,则会在map中放入collection、list、users三个键值对,这三个指向同一个内存地址即内容相同。
            if (((Map) parameterObject).containsKey("list")) {
                Map map = (Map) parameterObject;
                ArrayList list = (ArrayList) map.get("list");
                Object element = list.get(0);
                Class<?> elementClass = element.getClass();
                SensitiveData tempSensitiveData = AnnotationUtils.findAnnotation(elementClass, SensitiveData.class);
                if (Objects.nonNull(tempSensitiveData)) {
                    for (Object elementObject : list) {
                        Field[] declaredFields = elementClass.getDeclaredFields();
                        IEncryptUtil.encrypt(declaredFields, elementObject);
                    }
                }
            }

            //2. 如果使用了@Param注解对参数重命名为users,那么map中只会放入users、param1两个键值对,这2个指向同一个内存地址即内容相同。
            if (((Map) parameterObject).containsKey("param1")) {
                Map map = (Map) parameterObject;
                Object param1 = map.get("param1");
                //如果param1是ArrayList,则转为arrayList。否则不转换
                if (param1 instanceof ArrayList) {
                    ArrayList list = (ArrayList) param1;
                    Object element = list.get(0);
                    Class<?> elementClass = element.getClass();
                    SensitiveData tempSensitiveData = AnnotationUtils.findAnnotation(elementClass, SensitiveData.class);
                    if (Objects.nonNull(tempSensitiveData)) {
                        for (Object elementObject : list) {
                            Field[] declaredFields = elementClass.getDeclaredFields();
                            IEncryptUtil.encrypt(declaredFields, elementObject);
                        }
                    }
                }
            }
        }
    }

    

这里需要注意,不能使用parameterObject instanceof ListparameterObject instanceof ArrayList来判断是否参数是否是列表类型。理由如下:

  • 如果没有对dao方法的参数重命名,则mybatis会把userslistcollection(都是List)封装到一个ParamMap中,并且list和collection与user指向同一个内存地址,即数据完全相同。
    int insertBatch(List<User> users);

在这里插入图片描述

  • 如果使用了@Param("users")对dao层的方法参数进行了重命名,则mybatis会把list封装到一个ParamMap中,这个map中不会有listcollection,而是存入当前参数名usersparam1的list(若有多个参数则继续 user2param2…)
    int insertBatchByParam(@Param("users") List<User> users);

在这里插入图片描述

2. 防止重复加密或解密

同一个对象在进行过dao层的更新后,进行了一次加密,后续如果再用该对象进行更新操作,又会被加密一次,这导致加密了两次,而且解密不出错。
解决方法是:在加密过的字段前添加加密标识,加解密的时候先判断是否被加密过。

比如:

    @Override
    public int saveOrUpdateUser(){
        User user = new User(31,"小二", "111.com", "123411112222");
        int result = userDao.updateUserByPrimaryKey(user);
        log.info("user: {}", user);
        if (result != 1){
            result = userDao.addUser(user);
            log.info("user: {}", user);
        }
        return result;
    }

因此需要加密前需要判断(解密同理):

try {
    if(!value.startsWith(DBAESUtil.KEY_SENSITIVE)) {
        encrypt = DBAESUtil.encrypt(value);
        encrypt = DBAESUtil.KEY_SENSITIVE + encrypt;
    }
    //修改字段值
    field.set(paramsObject, encrypt);
} catch (Exception e) {
    e.printStackTrace();
}             

参考链接:

  1. https://blog.csdn.net/relosy/article/details/123494036
  2. https://blog.csdn.net/relosy/article/details/123494036
  3. https://blog.csdn.net/wtmdcnm/article/details/115211183
  4. https://blog.csdn.net/qq_45454294/article/details/122012444

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1087074.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

光伏三相并网逆变器的控制策略与性能分析

微❤关注“电击小子程高兴的MATLAB小屋”获得资料&#xff08;专享优惠&#xff09; 光伏三相并网逆变器的控制策略与性能分析 引言&#xff1a; 随着可再生能源的日益重视和发展&#xff0c;光伏发电系统在电力系统中的地位越来越重要。其中&#xff0c;光伏三相并网逆变器…

什么台灯最好学生晚上用?开学适合孩子学习的台灯

作为学龄期儿童的家长&#xff0c;最担心的就是孩子长时间学习影响视力健康。无论是上网课、写作业、玩桌游还是陪伴孩子读绘本&#xff0c;都需要一个足够明亮的照明环境&#xff0c;因此选购一款为孩子视力发展保驾护航的台灯非常重要。推荐五款适合孩子学习的台灯。 1. 书客…

distcc分布式编译

distcc https://gitee.com/bison-fork/distcc.git 下载工具链 mingw&#xff0c;https://www.mingw-w64.org/downloads/#w64devkitperl&#xff0c;https://strawberryperl.com/releases.html免安装zip版本&#xff0c;autoconf等脚本依赖perlautoconf、automake&#xff0c…

软件测试学习(四)自动测试和测试工具、缺陷轰炸、外包测试、计划测试工作、编写和跟踪测试用例

目录 自动测试和测试工具 工具和自动化的好处 测试工具 查看器和监视器 驱动程序 桩 压力和负载工具 干扰注入器和噪声发生器 分析工具 软件测试自动化 宏录制和回放 可编程的宏 完全可编程的自动测试工具 随机测试&#xff1a;猴子和大猩猩 使用测试工具和自动…

SpringBoot 前端406 后端Could not find acceptable representation

原因:返回对象没有get方法,无法转成JSON格式

2023年10月13日历史上的今天大事件早读

54年10月13日罗马帝国皇帝克劳狄一世逝世 467年10月13日北魏孝文帝拓跋宏出生 982年10月13日辽国皇帝辽景宗逝世 1847年10月13日李沅发起义 1884年10月13日国际标准时间日 1925年10月13日前英国首相撒切尔夫人出生 1927年10月13日鲁丝-埃尔德飞渡大西洋失败 1928年10月1…

电脑怎么剪辑视频?高手分享的独家秘诀

视频剪辑是一项有趣而具有创造性的活动&#xff0c;可以帮助您将录制的视频片段转化为有趣、有启发性的作品。无论您是想创建家庭影片、Vlog视频、教程&#xff0c;还是其他任何类型的视频&#xff0c;掌握视频剪辑技巧都是必要的。那电脑怎么剪辑视频呢&#xff1f;在本篇文章…

竞赛 深度学习 机器视觉 车位识别车道线检测 - python opencv

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 深度学习 机器视觉 车位识别车道线检测 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xff01; &#x1f947;学长这里给一个题目综合评分(每项满分5分) …

QTableWidget中cell 和 item区别

1.cell&#xff1a;某行某列中单元格。cell相当于一个容器&#xff0c;如箱子。里面不管有没有东西&#xff0c;cell都在那里。 2.item&#xff1a;item是某行某列单元格中的内容&#xff0c;即cell箱子中所放置的东西&#xff0c;即实实在在的东西。 通过调用 itemClicked()…

VMware虚拟机安装Linux教程(史上最全)

前言 许多想学网络安全的小伙伴&#xff0c;因为自己电脑是Windows系统的&#xff0c;并且电脑里面有很多重要文件&#xff0c;一般这种情况最好是安装一个虚拟机&#xff0c;然后虚拟机安装Linux的操作系统&#xff0c;这样就不会对原本的系统和文件产生影响。 需要网络安全…

云原生网关可观测性综合实践

作者&#xff1a;钰诚 可观测性 可观测性&#xff08;Observability&#xff09;是指系统、应用程序或服务的运行状态、性能和行为能够被有效地监测、理解和调试的能力。 随着系统架构从单体架构到集群架构再到微服务架构的演进&#xff0c;业务越来越庞大&#xff0c;也越来…

【Unity实战100例】Unity内部软键盘输入制作

目录 一. 样式颜色设置 二. UI逻辑 源码地址: 哔哩哔哩工房 一. 样式颜色设置 可以在预制体上提前设置一下对应组件的颜色,包括按键边框的颜色,内部填充色,普通按键文本颜色,功能按键文本颜色,大家可以根据自己的需求处理按钮逻辑。

基于PHP+laravel+vue自主研发的医院手术麻醉信息系统源码

大型医院AIMS手术麻醉信息系统全套成品源码 开发语言&#xff1a;PHP、 js 技术架构&#xff1a;mysqllaravelvue2 开发工具&#xff1a;oh-storm 前端框架&#xff1a;vue2 element 后端框架&#xff1a;laravel 数 据 库&#xff1a;mysql 8.0 手术麻醉临床信息系统…

【小余送书第三期】CTF/AWD竞赛标准参考书+实战指南:《AWD特训营》,参与活动,领书咯!

目录 一、背景介绍 二、内容简介 三、读者对象 四、本书目录 五、书籍概览 一、背景介绍 随着网络安全问题日益凸显&#xff0c;国家对网络安全人才的需求持续增长&#xff0c;其中&#xff0c;网络安全竞赛在国家以及企业的人才培养和选拔中扮演着至关重要的角色。 在数…

virtuoso如何导出def?

我正在「拾陆楼」和朋友们讨论有趣的话题,你⼀起来吧? 拾陆楼知识星球入口 数模混合的项目中需要把PAD位置通过def的形式读入pr设计中,以此让power plan规避PAD的区域,避免DRC问题。 使用virtuoso导出def的流程如下: 1)新建一个空cell,在Library Manager-File-New-Cell…

(七)Python函数和lambda表达式

函数就是一段封装好的&#xff0c;可以重复使用的代码&#xff0c;它使得我们的程序更加模块化&#xff0c;不需要编写大量重复的代码。 函数可以提前保存起来&#xff0c;并给它起一个独一无二的名字&#xff0c;只要知道它的名字就能使用这段代码。函数还可以接收数据&#…

AGI之MFM:《多模态基础模型:从专家到通用助手》翻译与解读之视觉理解、视觉生成

​AGI之MFM&#xff1a;《Multimodal Foundation Models: From Specialists to General-Purpose Assistants多模态基础模型&#xff1a;从专家到通用助手》翻译与解读之视觉理解、视觉生成 目录 相关文章 AGI之MFM&#xff1a;《Multimodal Foundation Models: From Speciali…

使用newman来执行posman脚本

一.什么是Newman 简单来说使用Newman&#xff0c;可以让我们的Postman的脚本通过非GUI(命令行)的方式运行 二、安装 首先需要安装NodeJsNodeJs安装完成后&#xff0c;使用npm install -g newman 安装newman 三、NewMan命令 使用newman run --help 可以查看帮助命令 -e 指定环境…

fabric.js的使用

安装&#xff1a;npm install fabric --save // 使用fabric实现&#xff1a; import { fabric } from fabricinitFabric () {// create a wrapper around native canvas element (with id"canvasEl")let canvas new fabric.Canvas(canvasEl)// create a rectangle …

JVM 入门

文章目录 JVMJVM 垮平台执行其他语言的代码JDK、JRE、JVM各种 Java 虚拟机查看我们本地的虚拟机版本HotSpot 的整体架构 JVM JVM &#xff08;Java Virtual Machine&#xff09;&#xff0c;Java 虚拟机&#xff0c;我们的 Java 代码需编译为 .class 字节码文件&#xff0c;经…