⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/relosy/article/details/123494036 「shenyang1026」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

问题

在项目中需要对用户敏感数据进行脱敏处理,例如身份号、手机号等信息进行加密再入库。

解决思路

  • 就是:一种最简单直接的方式,在所有涉及数据敏感的查询到对插入时进行密码加解密
  • 方法二:有方法一到出现对所有重大问题的影响,需要考虑到问题的出现,并且需要考虑可能出现的组员时添加数据的方法。

最后决定采用mybatis的插件在mybatis的SQL执行和结果填充操作上进行切入。上层业务调用不再需要考虑数据的加敏同时也保证了数据的加解密

Mybatis 插件原理

Mybatis 的是通过拦截器实现的,Mabatis 支持对当事人进行拦截

实现

  • 设置对参数中带有敏感参数字段的数据时进行加密
  • 对返回的结果进行解密处理

根据不同的要求,我们只需要对ParameterHandlerResultSetHandler进行切入。

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

加注解

定义SensitiveData注解

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

}

定义EncryptTransaction注解

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

}

加解密工具类

解密

package sicnu.cs.ich.common.interceptor.transaction.service;

public interface IDecryptUtil {
/**
* 解密
*
* @param result resultType的实例
* @return T
* @throws IllegalAccessException 字段不可访问异常
*/
<T> T decrypt(T result) throws IllegalAccessException;
}

加密接口

package sicnu.cs.ich.common.interceptor.transaction.service;

import java.lang.reflect.Field;

public interface IEncryptUtil {
/**
* 加密
*
* @param declaredFields 加密字段
* @param paramsObject 对象
* @param <T> 入参类型
* @return 返回加密
* @throws IllegalAccessException 不可访问
*/
<T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException;
}
package sicnu.cs.ich.common.interceptor.transaction.service.impl;

import org.springframework.stereotype.Component;
import sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction;
import sicnu.cs.ich.common.interceptor.transaction.service.IDecryptUtil;
import sicnu.cs.ich.common.util.keyCryptor.DBAESUtil;

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

@Component
public class DecryptImpl implements IDecryptUtil {

/**
* 解密
*
* @param result resultType的实例
*/
@Override
public <T> T decrypt(T result) throws IllegalAccessException {
//取出resultType的类
Class<?> resultClass = result.getClass();
Field[] declaredFields = resultClass.getDeclaredFields();
for (Field field : declaredFields) {
//取出所有被DecryptTransaction注解的字段
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;
}
}

加密实现类

package sicnu.cs.ich.common.interceptor.transaction.service.impl;

import com.fasterxml.jackson.databind.ObjectReader;
import org.springframework.stereotype.Component;
import sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction;
import sicnu.cs.ich.common.interceptor.transaction.service.IEncryptUtil;
import sicnu.cs.ich.common.util.keyCryptor.DBAESUtil;

import java.io.ObjectInputStream;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Objects;
import java.util.Random;

@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;
}
}

模拟类

package sicnu.cs.ich.common.interceptor.transaction.service.impl;

import org.springframework.stereotype.Component;
import sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction;
import sicnu.cs.ich.common.interceptor.transaction.service.IDecryptUtil;
import sicnu.cs.ich.common.util.keyCryptor.DBAESUtil;

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

@Component
public class DecryptImpl implements IDecryptUtil {

/**
* 解密
*
* @param result resultType的实例
*/
@Override
public <T> T decrypt(T result) throws IllegalAccessException {
//取出resultType的类
Class<?> resultClass = result.getClass();
Field[] declaredFields = resultClass.getDeclaredFields();
for (Field field : declaredFields) {
//取出所有被DecryptTransaction注解的字段
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;
}
}

加解密工具类

package sicnu.cs.ich.common.util.keyCryptor;

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

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);
}

}

插件实现

参数插件ParameterInterceptor

切入mybatis设置参数时对敏感数据进行加密

Mybatis插件的使用就是通过实现Mybatis中的Interceptor接口

@Intercepts注解

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

import com.baomidou.mybatisplus.core.MybatisParameterHandler;
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 sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction;
import sicnu.cs.ich.api.common.annotations.transaction.SensitiveData;
import sicnu.cs.ich.common.interceptor.transaction.service.IEncryptUtil;
import sicnu.cs.ich.common.util.keyCryptor.DBAESUtil;
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.*;

@Slf4j
// 注入Spring
@Component
@Intercepts({
@Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
})
public class ParameterInterceptor implements Interceptor {

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

package sicnu.cs.ich.common.interceptor.transaction;

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 sicnu.cs.ich.api.common.annotations.transaction.SensitiveData;

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

@Slf4j
@Component
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class ResultSetInterceptor implements Interceptor {

@Autowired
private sicnu.cs.ich.common.interceptor.transaction.service.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) {

}
}

使用

注意解在实体类上

import lombok.*;
import org.springframework.security.core.userdetails.UserDetails;
import sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction;
import sicnu.cs.ich.api.common.annotations.transaction.SensitiveData;


@With
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
@SensitiveData // 插件只对加了该注解的类进行扫描,只有加了这个注解的类才会生效
public class User implements Serializable {
private Integer id;
private String username;
private String openId;
private String password;
// 表明对该字段进行加密
@EncryptTransaction
private String email;
// 表明对该字段进行加密
@EncryptTransaction
private String mobile;
private Date createTime;
private Date expireTime;
private Boolean status = true;
}

注解在参数上

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction;

@Mapper
public interface UserMapper extends BaseMapper<User> {
// 只需要在参数前加上@EncryptTransaction 即可
long countByEmail(@EncryptTransaction @Param("email") String email);

long countByMobile(@EncryptTransaction @Param("mobile") String mobile);

}

文章目录
  1. 1. 问题
  2. 2. 解决思路
  3. 3. Mybatis 插件原理
  4. 4. 实现
  5. 5. 加注解
  6. 6. 加解密工具类
  7. 7. 插件实现
    1. 7.0.1. 参数插件ParameterInterceptor
  • 8. 使用