当前位置: 首页 > news >正文

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

一、介绍

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

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

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

二、解决思路

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

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

  1. 采用mybatis插件在mybatis SQL执行和查询结果填充操作上进行切入
  2. 使用mybatis框架提供的TypeHandler来实现在持久层处理数据 (见https://blog.csdn.net/qq_39052947/article/details/128148805)
  3. 采用mybatis的插件在mybatis SQL执行和查询结果填充操作上进行切入

本文介绍第3种方式,即使用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 {@Autowiredprivate com.zsx.utils.IEncryptUtil IEncryptUtil;@Overridepublic Object intercept(Invocation invocation) throws Throwable {//@Signature 指定了 type= parameterHandler 后,这里的 invocation.getTarget() 便是parameterHandler//若指定ResultSetHandler ,这里则能强转为ResultSetHandlerMybatisParameterHandler 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;}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic 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 {@Autowiredprivate com.zsx.utils.IDecryptUtil IDecryptUtil;@Overridepublic Object intercept(Invocation invocation) throws Throwable {//取出查询的结果Object resultObject = invocation.proceed();if (Objects.isNull(resultObject)) {return null;}//基于selectListif (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);}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic 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 {@Overridepublic <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的实例*/@Overridepublic <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;// 表明对该字段进行加密@SensitiveFieldprivate String email;// 表明对该字段进行加密@SensitiveFieldprivate 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 {@Autowiredprivate 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层的更新后,进行了一次加密,后续如果再用该对象进行更新操作,又会被加密一次,这导致加密了两次,而且解密不出错。
解决方法是:在加密过的字段前添加加密标识,加解密的时候先判断是否被加密过。

比如:

    @Overridepublic 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

相关文章:

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

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

【特纳斯电子】基于物联网的指纹密码锁系统设计-实物设计

资料下载链接&#xff1a;基于物联网的指纹密码锁系统设计-实物设计 - 电子校园网 编号&#xff1a; T3732205M-SW 设计简介&#xff1a; 本设计是基于单片机的指纹密码锁&#xff0c;主要实现以下功能&#xff1a; 1、可通过密码解锁 2、可通过云平台解锁 3、可通过指纹解…...

【牛客面试必刷TOP101】Day9.BM37 二叉搜索树的最近公共祖先和BM42 用两个栈实现队列

作者简介&#xff1a;大家好&#xff0c;我是未央&#xff1b; 博客首页&#xff1a;未央.303 系列专栏&#xff1a;牛客面试必刷TOP101 每日一句&#xff1a;人的一生&#xff0c;可以有所作为的时机只有一次&#xff0c;那就是现在&#xff01;&#xff01;&#xff01;&…...

10.12 校招 实习 内推 面经

绿*泡*泡&#xff1a; neituijunsir 交流裙 &#xff0c;内推/实习/校招汇总表格 1、校招 | 2024届秋招&#xff0c;美团哪些校招岗位最缺人&#xff1f;&#xff08;内推&#xff09; 校招 | 2024届秋招&#xff0c;美团哪些校招岗位最缺人&#xff1f;&#xff08;内推&…...

redis 生成流水工具类

使用redis存储流水号&#xff0c;代码如下&#xff1a; import cn.hutool.core.date.DateUtil; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component;Component public class RedisSerialUtil {private RedisTemplate…...

BGP服务器租用腾讯云和阿里云价格对比

BGP云服务器像阿里云和腾讯云均是BGP多线网络&#xff0c;速度更快延迟更低&#xff0c;阿里云BGP服务器2核2G3M带宽优惠价格108元一年起&#xff0c;腾讯云BGP服务器2核2G3M带宽95元一年起&#xff0c;阿腾云atengyun.com分享更多云服务器配置如2核4G、4核8G、8核16G等配置价格…...

PyTorch 深度学习之多分类问题Softmax Classifier(八)

1. Revision: Diabetes dataset 2. Design 10 outputs using Sigmoid? 2.1 Output a Distribution of prediction with Softmax 2.2 Softmax Layer Example, 2.3 Loss Function-Cross Entropy Cross Entropy in Numpy Cross Entropy in PyTorch 注意交叉熵损失&#xff0c;最…...

抖音直播招聘小程序可以增加职位展示,提升转化率,增加曝光度

抖音直播招聘报白是指进入抖音的白名单&#xff0c;允许在直播间或小视频中发布招聘或找工作等关键词。否则会断播、不推流、限流。抖音已成为短视频流量最大的平台&#xff0c;但招聘企业数量较少。抖音招聘的优势在于职位以视频、直播方式展示&#xff0c;留存联系方式更加精…...

论文阅读之《Learn to see in the dark》

Learning to See in the Dark-CVPR2018 Chen ChenUIUC&#xff08;伊利诺伊大学厄巴纳-香槟分校&#xff09; Qifeng Chen, Jia Xu, Vladlen Koltun Intel Labs(英特尔研究院) 文章链接&#xff1a;https://arxiv.org/pdf/1805.01934.pdfhttps://arxiv.org/pdf/1805.01934.p…...

Docker 生成自定义镜像并使用Docker Compose部署

Docker 生成自定义镜像并使用Docker Compose部署 Docker Compose 是一个用于定义和运行多个 Docker 容器的工具&#xff0c;可以轻松管理复杂的应用程序。本文将介绍如何在 Docker Compose 中使用自定义 Docker 镜像&#xff0c;并提供了生成自定义 Docker 镜像的步骤。 步骤…...

设计模式~调停者(中介者)模式(Mediator)-21

调停者&#xff08;中介者&#xff09;模式(Mediator) &#xff08;1&#xff09;优点 &#xff08;2&#xff09;缺点 &#xff08;3&#xff09;使用场景 &#xff08;4&#xff09;注意事项&#xff1a; &#xff08;5&#xff09;应用实例&#xff1a; 代码 调停者&a…...

计算机毕业设计选什么题目好?springboot 医院门诊在线预约挂号系统

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…...

linux中使用ps查看进程的所有线程

在 Linux 系统中&#xff0c;可以使用 ps 命令和 ps H 命令结合来查看进程的线程信息。ps 命令用于显示系统中当前运行的进程信息&#xff0c;而 ps H 命令则可以显示进程中的所有线程。 使用以下命令可以查看指定进程的所有线程信息&#xff1a; ps H -T <PID>将 替换…...

本、硕、博区别真的辣么大吗?

61&#xff1a; 发际线已经说明了一切…… Super Mario&#xff1a; 小学&#xff0c;老师告诉学生&#xff1a;“森林里有只老虎&#xff0c;已经被我关在笼子里&#xff0c;我会带你去那个地方&#xff0c;然后给你一把猎枪&#xff0c;告诉你猎枪怎么用&#xff0c;并开枪…...

[Spring] SpringMVC 简介(一)

目录 一、SpringMVC 简介 1、什么是 MVC 2、什么是 SpringMVC 3、SpringMVC 实现原理 4、SpringMVC 的特点 二、简单案例 1、引入依赖 2、在 web.xml 中配置前端控制器 DispatcherServlet 3、创建 SpringMVC 的配置文件 4、创建请求控制器 5、测试页面 6、访问不到 …...

机器学习基础之《回归与聚类算法(2)—欠拟合与过拟合》

一、背景 1、上一篇说正规方程的时候&#xff0c;实际情况中使用很少&#xff0c;主要原因它不能解决过拟合。 2、训练集上表现的好&#xff0c;测试集上表现不好—过拟合 二、欠拟合和过拟合 1、欠拟合 训练集&#xff1a;有3个训练集&#xff0c;告诉机器都是天鹅 机器学…...

flutter dio 请求封装(空安全)

一、添加依赖 dio: ^5.3.2二、请求封装 class HttpHelper {static Dio? mDio;static BaseOptions? options;static HttpHelper? httpHelper;CancelToken cancelToken CancelToken();static const String GET get;static const String POST post;static const String PU…...

chatgpt GPT-4V是如何实现语音对话的

直接上代码 https://chat.openai.com/voice/get_token 1. 请求内容 Request:GET /voice/get_token HTTP/1.1 Host: ios.chat.openai.com Content-Type: application/json Cookie: _puiduser***Fc9T:16962276****Nph%2Fb**SU%3D; _uasid"Z0FBQUF***nPT0"; __cf_bmBUg…...

C++项目-求水仙花数

求水仙花数 #include <iostream> using namespace std;int main() {int n 100;do {int a 0;int b 0;int c 0;a n % 10; //个位b n / 10 % 10; //十位c n / 100 % 10; //百位if (a * a * a b * b * b c * c * c n) {cout << n << endl;}…...

从零开始基于LLM构建智能问答系统的方案

本文首发于博客 LLM应用开发实践 一个完整的基于 LLM 的端到端问答系统&#xff0c;应该包括用户输入检验、问题分流、模型响应、回答质量评估、Prompt 迭代、回归测试&#xff0c;随着规模增大&#xff0c;围绕 Prompt 的版本管理、自动化测试和安全防护也是重要的话题&#x…...

Android---Synchronized 和 ReentrantLock

Synchronized 基本使用 1. 修饰实例方法 public class SynchronizedMethods{private int sum 0;public synchronized void calculate(){sum sum 1;} } 这种情况下的锁对象是当前实例对象&#xff0c;因此只有同一个实例对象调用此方法才会产生互斥效果&#xff1b;不同的…...

【解题报告】牛客挑战赛70 maimai

题目链接 这个挑战赛的 F F F是我出的&#xff0c;最后 zhoukangyang 爆标了。。。orzorz 记所有有颜色的边的属性集合 S S S 。 首先在外层容斥&#xff0c;枚举 S ∈ [ 0 , 2 w ) S\in [0,2^w) S∈[0,2w)&#xff0c;计算被覆盖的的边中不包含 S S S 中属性&#xff0c…...

算启新程 智享未来 | 紫光展锐携手中国移动共创数智未来

10月11日-13日&#xff0c;2023年中国移动全球合作伙伴大会在广州举行&#xff0c;此次大会以“算启新程 智享未来”为主题&#xff0c;与合作伙伴一起共商融合创新&#xff0c;共创数智未来。作为中国移动每年规模最大、最具影响力的盛会&#xff0c;吸引了数百家世界500强企业…...

thinkphp5.1 获取缓存cache(‘cache_name‘)特别慢,php 7.0 unserialize 特别慢

thinkphp5.1 获取缓存cache(‘cache_name’)特别慢&#xff0c;php 7.0 unserialize 特别慢 场景&#xff1a; 项目中大量使用了缓存&#xff0c;本地运行非常快&#xff0c;二三百毫秒&#xff0c;部署到服务器后 一个表格请求就七八秒&#xff0c;最初猜想是数据库查询慢&am…...

【Linux】UNIX 术语中,换页与交换的区别和Linux 术语中,换页与交换的区别?

UNIX换页和交换的区别 在UNIX中&#xff0c;换页&#xff08;Paging&#xff09;是一种内存管理技术&#xff0c;用于在程序运行时动态地将其代码和数据从磁盘加载到内存中。当程序需要访问的页面不在内存中时&#xff0c;就会发生页错误&#xff08;page error&#xff09;&a…...

零基础学python之集合

文章目录 集合1、创建集合2、集合常见操作方法2、1 增加数据2、2 删除数据2、3 查找数据 3、总结 集合 目标 创建集合集合数据的特点集合的常见操作 1、创建集合 创建集合使用{}或set()&#xff0c; 但是如果要创建空集合只能使用set()&#xff0c;因为{}用来创建空字典。 …...

PromptScript:轻量级 DSL 脚本,加速多样化的 LLM 测试与验证

TL&#xff1b;DR 版本 PromptScript 是一个轻量级的 Prompt 调试用的 DSL &#xff08;Yaml&#xff09;脚本&#xff0c;以用于快速使用、构建 Prompt。 PromptScript 文档&#xff1a;https://framework.unitmesh.cc/prompt-script Why PromptScript &#xff1f; 几个月前&…...

强化学习(Reinforcement Learning)与策略梯度(Policy Gradient)

写在前面&#xff1a;本篇博文的内容来自李宏毅机器学习课程与自己的理解&#xff0c;同时还参考了一些其他博客(懒得放链接)。博文的内容主要用于自己学习与记录。 1 强化学习的基本框架 强化学习(Reinforcement Learning, RL)主要由智能体(Agent/Actor)、环境(Environment)、…...

JUC之ForkJoin并行处理框架

ForkJoin并行处理框架 Fork/Join 它可以将一个大的任务拆分成多个子任务进行并行处理&#xff0c;最后将子任务结果合并成最后的计算结果&#xff0c;并进行输出。 类似于mapreduce 其实&#xff0c;在Java 8中引入的并行流计算&#xff0c;内部就是采用的ForkJoinPool来实现…...

【牛客面试必刷TOP101】Day8.BM33 二叉树的镜像和BM36 判断是不是平衡二叉树

作者简介&#xff1a;大家好&#xff0c;我是未央&#xff1b; 博客首页&#xff1a;未央.303 系列专栏&#xff1a;牛客面试必刷TOP101 每日一句&#xff1a;人的一生&#xff0c;可以有所作为的时机只有一次&#xff0c;那就是现在&#xff01;&#xff01;&#xff01;&…...

CSS padding(填充)

CSS padding&#xff08;填充&#xff09;是一个简写属性&#xff0c;定义元素边框与元素内容之间的空间&#xff0c;即上下左右的内边距。 padding&#xff08;填充&#xff09; 当元素的 padding&#xff08;填充&#xff09;内边距被清除时&#xff0c;所释放的区域将会受到…...

C语言达到什么水平才能从事单片机工作

C语言达到什么水平才能从事单片机工作 从事单片机工作需要具备一定的C语言编程水平。以下是几个关键要点&#xff1a;基本C语言知识&#xff1a; 掌握C语言的基本语法、数据类型、运算符、流控制语句和函数等基本概念。最近很多小伙伴找我&#xff0c;说想要一些C语言学习资料&…...

Java架构师理解SAAS和多租户

目录 1 云服务的三种模式1.1 IaaS(基础设施即服务)1.2 PaaS(平台即服务)1.3 SaaS(软件即服务)1.4 区别与联系2 SaaS的概述2.1 Saas详解2.2 应用领域与行业前景2.3 Saas与传统软件对比3 多租户SaaS平台的数据库方案3.1 多租户是什么3.2 需求分析3.3 多租户的数据库方案分析…...

关于Java线程池相关面试题

【更多面试资料请加微信号&#xff1a;suns45】 https://flowus.cn/share/f6cd2cbe-627a-435f-a6e5-1395333f92e8 【FlowUs 息流】&#x1f4e3;suns-Java资料 访问密码&#xff1a;【请加微信号&#xff1a;suns45】 ————线程相关的面试题———— 0&#xff1a;创建线…...

ExcelBDD Python指南

在Python里面支持BDD Excel BDD Tool Specification By ExcelBDD Method This tool is to get BDD test data from an excel file, its requirement specification is below The Essential of this approach is obtaining multiple sets of test data, so when combined with…...

基于深度学习的驾驶员疲劳监测系统的设计与实现

点击以下链接获取源码&#xff1a; https://download.csdn.net/download/qq_64505944/88421622?spm1001.2014.3001.5503 基于深度学习的驾驶员疲劳监测系统的设计与实现 1 绪论 在21世纪&#xff0c;各国的经济飞速发展&#xff0c;人民越来越富裕&#xff0c;道路上的汽车也逐…...

B树、B+树详解

B树 前言   首先&#xff0c;为什么要总结B树、B树的知识呢&#xff1f;最近在学习数据库索引调优相关知识&#xff0c;数据库系统普遍采用B-/Tree作为索引结构&#xff08;例如mysql的InnoDB引擎使用的B树&#xff09;&#xff0c;理解不透彻B树&#xff0c;则无法理解数据…...

使用hugging face开源库accelerate进行多GPU(单机多卡)训练卡死问题

目录 问题描述及配置网上资料查找1.tqdm问题2.dataloader问题3.model(input)写法问题4.环境变量问题 我的卡死问题解决方法 问题描述及配置 在使用hugging face开源库accelerate进行多GPU训练&#xff08;单机多卡&#xff09;的时候&#xff0c;经常出现如下报错 [E Process…...

IDEA 修改插件安装位置

不说假话&#xff0c;一定要看到最后&#xff0c;不然你以为我为什么要自己总结&#xff01;&#xff01;&#xff01; IDEA 修改插件安装位置 前言步骤 前言 IDEA 默认的配置文件均安装在C盘&#xff0c;使用时间长会生成很多文件&#xff0c;这些文件会占用挤兑C盘空间&…...

牛客网SQL160

国庆期间每类视频点赞量和转发量_牛客题霸_牛客网 select * from ( select tag,dt, sum(单日点赞量)over(partition by tag order by dt rows between 6 preceding and 0 following), max(单日转发量)over(partition by tag order by dt rows between 6 preceding and 0 follo…...

HDFS Java API 操作

文章目录 HDFS Java API操作零、启动hadoop一、HDFS常见类接口与方法1、hdfs 常见类与接口2、FileSystem 的常用方法 二、Java 创建Hadoop项目1、创建文件夹2、打开Java IDEA1) 新建项目2) 选择Maven 三、配置环境1、添加相关依赖2、创建日志属性文件 四、Java API操作1、在HDF…...

论文阅读之【Is GPT-4 a Good Data Analyst?(GPT-4是否是一位好的数据分析师)】

文章目录 论文阅读之【Is GPT-4 a Good Data Analyst?&#xff08;GPT-4是否是一位好的数据分析师&#xff09;】背景&#xff1a;数据分析师工作范围基于GPT-4的端到端数据分析框架将GPT-4作为数据分析师的框架的流程图 实验分析评估指标表1&#xff1a;GPT-4性能表现表2&…...

【数据结构】:二叉树与堆排序的实现

1.树概念及结构(了解) 1.1树的概念 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合把它叫做树是因为它看起来像一棵倒挂的树&#xff0c;也就是说它是根朝上&#xff0c;而叶朝下的有一个特殊的结点&#…...

纯css手写switch

CSS 手写switch 纯css手写switchcss变量 纯css手写switch 思路&#xff1a; switch需要的元素有&#xff1a;开关背景、开关按钮。点击按钮后&#xff0c;背景色变化&#xff0c;按钮颜色变化&#xff0c;呈现开关打开状态。 利用typecheckbox&#xff0c;来实现switch效果(修…...

PyTorch 深度学习之处理多维特征的输入Multiple Dimension Input(六)

1.Multiple Dimension Logistic Regression Model 1.1 Mini-Batch (N samples) 8D->1D 8D->2D 8D->6D 1.2 Neural Network 学习能力太好也不行&#xff08;学习到的是数据集中的噪声&#xff09;&#xff0c;最好的是要泛化能力&#xff0c;超参数尝试 Example, Arti…...

LeetCode【438】找到字符串中所有字母异位词

题目&#xff1a; 注意&#xff1a;下面代码勉强通过&#xff0c;每次都对窗口内字符排序。然后比较字符串。 代码&#xff1a; public List<Integer> findAnagrams(String s, String p) {int start 0, end p.length() - 1;List<Integer> result new ArrayL…...

关于LEFT JOIN的一次理解

先看一段例子&#xff1a; SELECTproduct_half_spu.id AS halfSpuId,product_half_spu.half_spu_code,product_half_spu.half_spu_name,COUNT( product_sku.id ) AS skuCount,product_half_spu.create_on,product_half_spu.create_by,product_half_spu.upload_pic_date,produc…...

各报文段格式集合

数据链路层-- MAC帧 前导码8B&#xff1a;数据链路层将封装好的MAC帧交付给物理层进行发送&#xff0c;物理层在发送MAC帧前&#xff0c;还要在前面添加8字节的前导码&#xff08;分为7字节的前同步码1字节的帧开始定界符&#xff09;MAC地址长度6B数据长度46&#xff5e;1500B…...

【算法-动态规划】最长公共子序列

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kuan 的首页,持续学…...

区块链游戏的开发流程

链游&#xff08;Blockchain Games&#xff09;的开发流程与传统游戏开发有许多相似之处&#xff0c;但它涉及到区块链技术的集成和智能合约的开发。以下是链游的一般开发流程&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&…...