一、介绍
业务场景中经常会遇到诸如用户手机号,身份证号,银行卡号,邮箱,地址,密码等等信息,属于敏感信息,需要保存在数据库中。而很多公司会会要求对数据库中的此类数据进行加密存储。
敏感数据脱敏需要处理的两个问题:
- 查询操作,需要对查询的关键字进行加密,同时也要对从库中查到的数据进行解密
- 插入和更新操作,需要对插入或者更新的数据进行加密,然后保存到数据库。
二、解决思路
- 最简单的方式:对代码中涉及的敏感数据接口在查询和插入、更新时进行加解密。缺点是工作量大,代码侵入多
- 在mybatis中进行统一拦截,上层业务调用不需要再考虑敏感数据的加解密问题。
通过查阅资料,发现思路二目前普遍有两种处理方式:
- 采用mybatis插件在mybatis SQL执行和查询结果填充操作上进行切入
- 使用mybatis框架提供的TypeHandler来实现在持久层处理数据
本文介绍第一种方式,即使用Mybatis的插件,通过拦截器实现敏感数据加解密
三、mybatis插件原理
Mybatis的插件
,是采用责任链机制
,通过JDK动态代理
来实现的。默认情况下,Mybatis允许使用插件来拦截四个对象:
Executor
:执行CURD操作;StatementHandler
:处理sql语句预编译,设置参数等相关工作;ParameterHandler
:设置预编译参数用的;ResultSetHandler
:处理结果集。
编写插件需要标识拦截方法和实现拦截逻辑。
标识拦截拦截方法是通过注解org.apache.ibatis.plugin.Intercepts
和注解org.apache.ibatis.plugin.Signature
实现的。
四、实现
- 设置参数时对参数中含有敏感字段的数据进行加密
- 对查询返回的结果进行解密处理
基于上面两种要求,我们只需要对ParameterHandler
和ResultSetHandler
进行切入。
定义特定注解,在切入时只需要检查字段中是否包含该注解来决定是否加解密
加解密注解
包含两个注解:
SensitiveData
注解:用在实体类上,表示此类有些字段需要加密,需要结合@EncryptTransaction
一起使用EncryptTransaction
注解:用在类的字段上或者方法的参数上,表示该字段或参数需要加密
1. 定义SensitiveData注解
package com.zsx.annotation;import java.lang.annotation.*;
/*** 该注解定义在类上* 插件通过扫描类对象是否包含这个注解来决定是否继续扫描其中的字段注解* 这个注解要配合EncryptTransaction注解* @author zhousx* @create 2023/10/01-22:45**/
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {}
2. 定义EncryptTransaction注解
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 EncryptTransaction {}
插件实现
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)
方法
package com.zsx.intercepter;import com.baomidou.mybatisplus.core.MybatisParameterHandler;
import com.zsx.annotation.EncryptTransaction;
import com.zsx.annotation.SensitiveData;
import com.zsx.utils.DBAESUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.binding.MapperMethod;
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.Array;
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 EncryptTransaction) {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
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. 加密接口
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. 解密接口
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. 加密实现类
package com.zsx.utils;import com.fasterxml.jackson.databind.ObjectReader;
import com.zsx.annotation.EncryptTransaction;
import org.springframework.stereotype.Component;
import java.io.ObjectInputStream;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Objects;
import java.util.Random;/*** @author zhousx* @create 2023/10/01-22:45**/
@Component
public class EncryptUtilImpl implements IEncryptUtil {@Overridepublic <T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException {//取出所有被EncryptTransaction注解的字段for (Field field : declaredFields) {EncryptTransaction encryptTransaction = field.getAnnotation(EncryptTransaction.class);if (!Objects.isNull(encryptTransaction)) {field.setAccessible(true);Object object = field.get(paramsObject);//暂时只实现String类型的加密if (object instanceof String) {String value = (String) object;//加密try {field.set(paramsObject, DBAESUtil.encrypt(value));} catch (Exception e) {e.printStackTrace();}}}}return paramsObject;}
}
4. 解密实现类
package com.zsx.utils;import com.zsx.annotation.EncryptTransaction;
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注解的字段EncryptTransaction encryptTransaction = field.getAnnotation(EncryptTransaction.class);if (!Objects.isNull(encryptTransaction)) {field.setAccessible(true);Object object = field.get(result);//String的解密if (object instanceof String) {String value = (String) object;//对注解的字段进行逐一解密try {field.set(result, DBAESUtil.decrypt(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 {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.EncryptTransaction;
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;// 表明对该字段进行加密@EncryptTransactionprivate String email;// 表明对该字段进行加密@EncryptTransactionprivate String phone;}
2. 注解在参数上
package com.zsx.mapper;import com.zsx.annotation.EncryptTransaction;
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(@EncryptTransaction @Param("phone") String phone);/*** 测试插入 普通参数加密,多个需要加密的字段* @param name* @param email* @param phone* @return*/int insertUserByParam(@Param("name") String name, @EncryptTransaction @Param("email") String email, @EncryptTransaction @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.EncryptTransaction;
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 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(@EncryptTransaction @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, @EncryptTransaction @Param("email") String email, @EncryptTransaction @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>
本文内容主要参考shenyang1026的博客: https://blog.csdn.net/relosy/article/details/123494036
其他参考:
- https://blog.csdn.net/relosy/article/details/123494036
- https://blog.csdn.net/wtmdcnm/article/details/115211183