当前位置: 首页 > 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;&…...