在项目中需要对用户敏感数据进行脱敏处理,例如身份证号、手机号等信息进行加密再入库
简介
功能:mybatis插件实现数据脱敏
作用:实现数据库中敏感数据的脱敏
目标群体:java开发工程师
使用场景:例如身份证号、手机号、用户密码、银行卡号等信息进行加密再入库
安装和配置
springboot/springcloud+mybatisplus+mysql项目
步骤1:搭建springboot项目
在使用该技术之前,您需要有springboot或者springcloud的项目;
步骤2:引入对应jar包
在使用该技术之前,您需要创建一个用于存储网站文件的目录;
<properties>
<mybatis-version>3.4.1</mybatis-version>
<mybatis-plus-generator.version>3.5.2</mybatis-plus-generator.version>
<mysql.version>8.0.22</mysql.version>
</properties>
<dependencies>
<!--mybatis-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus-generator.version}</version>
</dependency>
<!--mysql begin-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
</dependencies>
</project>
步骤3:创建数据库test和表user为例
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`password` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 53 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
步骤4:完成安装
现在,您可以使用该技术并按照屏幕上的说明进行操作了。
使用mybatis插件原理指南
1、 设置参数时对参数中含有敏感字段的数据进行加密;
2、 对查询返回的结果进行解密处理;
基于上面两种要求,我们只需要对ParameterHandler
和ResultSetHandler
进行切入。
定义特定注解,在切入时只需要检查字段中是否包含该注解来决定是否加解密
技术实现
自定义注解SensitiveData,该注解放在实体类上
/**
* 该注解定义在类上
* 插件通过扫描类对象是否包含这个注解来决定是否继续扫描其中的字段注解
* 这个注解要配合EncryptTransaction注解
*
**/
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SensitiveData {
}
自定义注解EncryptTransaction,放到需要加密的参数上面
/**
* 该注解有两种使用方式
* ①:配合@SensitiveData加在类中的字段上
* ②:直接在Mapper中的方法参数上使用
*
**/
@Documented
@Inherited
@Target({
ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptTransaction {
}
加解密工具类
/**
* 加密接口工具类
*
*/
public interface IEncryptUtil {
/**
* 加密
*
* @param declaredFields 加密字段
* @param paramsObject 对象
* @param <T> 入参类型
* @return 返回加密
* @throws IllegalAccessException 不可访问
*/
<T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException;
}
/**
* 加密实现类
*
*/
@Component
public class EncryptUtilImpl implements IEncryptUtil {
@Override
public <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;
}
}
/**
* 解密接口工具类
*/
public interface IDecryptUtil {
/**
* 解密
*
* @param result resultType的实例
* @return T
* @throws IllegalAccessException 字段不可访问异常
*/
<T> T decrypt(T result) throws IllegalAccessException;
}
/**
* 解密实现类
*/
@Component
public class DecryptUtilImpl implements IDecryptUtil {
@Override
public <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;
}
}
加密工具类
/**
* 加解密工具类
*/
public class DBAESUtil {
private static final String DEFAULT_V = "6859505890402435";
// 自己填写
private static final String KEY = "1234567890";
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);
}
}
插件实现
参数插件ParameterInterceptor
切入mybatis设置参数时对敏感数据进行加密
Mybatis插件的使用就是通过实现Mybatis中的Interceptor
接口
再配合@Intercepts
注解
// 使用mybatis插件时需要定义签名
// type标识需要切入的Handler
// method表示要要切入的方法
@Intercepts({
@Signature(type = ParameterHandler.class, method = “setParameters”, args = PreparedStatement.class),
})
/**
* 自定义参数拦截器
*/
@Slf4j
@Component
@Intercepts({
@Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
})
public class ParameterInterceptor implements Interceptor {
@Resource
private 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);
}
// 对普通字段进行加密
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();
}
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;
}
}
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;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
返回值插件ResultSetInterceptor
/**
* 返回值插件
*
*/
@Slf4j
@Component
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class ResultSetInterceptor implements Interceptor {
@Resource
private 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) {
}
}
使用
引入类上注解@SensitiveData和参数注解@EncryptTransaction
/**
* @Description User
*
**/
@Data
@TableName("user")
@SensitiveData
public class User implements Serializable {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField(value = "username")
private String userName;
@TableField(value = "password")
@EncryptTransaction
private String password;
}
或者注解在参属上
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 只需要在参数前加上@EncryptTransaction 即可
long countByEmail(@EncryptTransaction @Param("email") String email);
long countByMobile(@EncryptTransaction @Param("mobile") String mobile);
}