一.需求背景

跟大学室友闲谈时,了解到他公司正在做项目内对数据库敏感字段实现自动加解密的需求,使用的技术是Springboot,Mybatis-Plus,MySql等技术栈,加密算法是用的AES,密钥是放在华为云,这里实现一个阉割版的demo,仅供有兴趣的同学进行参考。

二.前置条件

首先我自己在日常搭了一个普通的springboot项目,目前还没有前台,所以就在浏览器请求tomcat模拟接口。

其次,这里的是实现主要是应用了Mybatis的拦截器,AES算法,mysql等技术栈,需要了解一下相关背景。

1.AES算法

AES的全称是Advanced Encryption Standard,意思是高级加密标准。它的出现主要是为了取代DES加密算法的,因为我们都知道DES算法的密钥长度是56Bit,因此算法的理论安全强度是2的56次方。但二十世纪中后期正是计算机飞速发展的阶段,元器件制造工艺的进步使得计算机的处理能力越来越强,虽然出现了3DES的加密方法,但由于它的加密时间是DES算法的3倍多,64Bit的分组大小相对较小,所以还是不能满足人们对安全性的要求。于是1997年1月2号,美国国家标准技术研究所宣布希望征集高级加密标准,用以取代DES。AES也得到了全世界很多密码工作者的响应,先后有很多人提交了自己设计的算法。最终有5个候选算法进入最后一轮:Rijndael,Serpent,Twofish,RC6和MARS。最终经过安全性分析、软硬件性能评估等严格的步骤,Rijndael算法获胜。

在密码标准征集中,所有AES候选提交方案都必须满足以下标准:

  • 分组大小为128位的分组密码。
  • 必须支持三种密码标准:128位、192位和256位。
  • 比提交的其他算法更安全。
  • 在软件和硬件实现上都很高效。

2.Mybatis拦截器

拦截器(Interceptor)在 Mybatis 中被当做插件(plugin)对待,官方文档提供了 Executor(拦截执行器的方法),ParameterHandler(拦截参数的处理),ResultSetHandler(拦截结果集的处理),StatementHandler(拦截Sql语法构建的处理) 共4种,并且提示“这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码”。

拦截器的使用场景主要是更新数据库的通用字段,分库分表,加解密等的处理。

1.1 MyBatis自定义拦截器

  • 实现org.apache.ibatis.plugin.Interceptor接口。
  • 添加拦截器注解org.apache.ibatis.plugin.Intercepts
  • 配置文件中添加拦截器

1.2 在MyBatis中可被拦截的类型有四种(按照拦截顺序)

  • Executor:拦截执行器的方法。
  • ParameterHandler:拦截参数的处理。
  • ResultHandler:拦截结果集的处理。
  • StatementHandler:拦截Sql语法构建的处理,绝大部分我们是在这里设置我们的拦截器

先执行每个插件的plugin方法,若是@Intercepts注解标明需要拦截该对象,那么生成类型对象的代理对象。(即使该插件需要拦截该类型对象,但是依旧会执行下一个插件的plugin方法)。知道执行完毕所有的plugin方法。在执行每个Intercept方法。

1.3 拦截器注解的作用:

自定义拦截器必须使用MyBatis提供的注解来声明我们要拦截的类型对象。

Mybatis插件都要有Intercepts注解来指定要拦截哪个对象哪个方法。我们知道,Plugin.wrap方法会返回四大接口对象的代理对象,会拦截所有的方法。在代理对象执行对应方法的时候,会调用InvocationHandler处理器的invoke方法。

1.4 拦截器注解的规则:

具体规则如下:

@Intercepts({
    @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
    @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
    @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})
})
复制代码
  • @Intercepts:标识该类是一个拦截器
  • @Signature:指明自定义拦截器需要拦截哪一个类型,哪一个方法;
  • type:对应四种类型中的一种;
  • method:对应接口中的哪类方法(因为可能存在重载方法);
  • args:对应哪一个方法;

1.5. 拦截器可拦截的方法

拦截的类

拦截的方法

Executor

update, query, flushStatements, commit, rollback,getTransaction, close, isClosed

ParameterHandler

getParameterObject, setParameters

StatementHandler

prepare, parameterize, batch, update, query

ResultSetHandler

handleResultSets, handleOutputParameters

Executor 提供的方法中,update 包含了 新增,修改和删除类型,无法直接区分,需要借助 MappedStatement 类的属性 SqlCommandType 来进行判断,该类包含了所有的操作类型

public enum SqlCommandType {
  UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
}


毕竟新增和修改的场景,有些参数是有区别的,比如创建时间和更新时间,update 时是无需兼顾创建时间字段的。

3.mysql数据库创建表

create table user
(
    id          bigint unsigned auto_increment comment '主键'
        primary key,
    name        varchar(20)                                    not null comment '姓名',
    balance     int         default 0                          not null comment '账户余额',
    password    varchar(50) not null comment '密码',
    create_time datetime    default CURRENT_TIMESTAMP          not null comment '创建时间',
    update_time datetime    default CURRENT_TIMESTAMP          null on update CURRENT_TIMESTAMP comment '更新时间'
)
    comment '用户表';

这里的password就是我们要加解密的字段,存储时要进行加密,获取查询后进行解密。

4.Maven依赖

<!--mybatis-plus依赖 -->
        <dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.4.0</version>
		</dependency>
        <dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.4.0</version>
		</dependency>
		<!--Mysql jdbc驱动-->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.17</version>
		</dependency>

三.代码实现

实现思路就是首先通过注解,能确定需要加解密的表和字段,然后通过自定义注解进行标注,通过自定义拦截器对字段进行加解密操作。

1.AES加解密工具类

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

/**
 * 加解密工具类
 * @author lima
 * @date 2023/3/3 16:56
 */
public class AESUtils {
    private static final String ALGORITHM = "AES";
    private static final String SECRET_KEY = "aes-key-lima1995"; // 密钥

    /**
     * 加密
     */
    public static String encrypt(String value) throws Exception {
        SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(), ALGORITHM);
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, keySpec);
        byte[] encrypted = cipher.doFinal(value.getBytes());
        return Base64.getEncoder().encodeToString(encrypted);
    }

    /**
     * 解密
     */
    public static String decrypt(String value) throws Exception {
        SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(), ALGORITHM);
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, keySpec);
        byte[] decoded = Base64.getDecoder().decode(value);
        byte[] decrypted = cipher.doFinal(decoded);
        return new String(decrypted);
    }
}

2.自定义注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 带有敏感字段的类需要加这个注解
 * @author lima
 * @date 2023/3/6 16:18
 */
@Inherited
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 需要加解密的字段用这个注解
 * @author lima
 * @date 2023/3/3 17:29
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Encrypted {

}

3.加密拦截器

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.sql.PreparedStatement;
import java.util.Objects;

/**
 * @author lima
 * @date 2023/3/6 16:19
 */
@Slf4j
@Component
@Intercepts({
        @Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}),
})
public class EncryptInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
            // 获取参数对像,即 mapper 中 paramsType 的实例
            Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");
            parameterField.setAccessible(true);
            //取出实例
            Object parameterObject = parameterField.get(parameterHandler);
            if (parameterObject != null) {
                Class<?> parameterObjectClass = parameterObject.getClass();
                //校验该实例的类是否被@SensitiveData所注解
                SensitiveData sensitiveData = AnnotationUtils.findAnnotation(parameterObjectClass, SensitiveData.class);
                if (Objects.nonNull(sensitiveData)) {
                    //取出当前当前类所有字段,传入加密方法
                    Field[] declaredFields = parameterObjectClass.getDeclaredFields();
                    encrypt(declaredFields, parameterObject);
                }
            }
            return invocation.proceed();
        } catch (Exception e) {
            log.error("加密失败", e);
        }
        return invocation.proceed();
    }

    /**
     * 切记配置,否则当前拦截器不会加入拦截器链
     */
    @Override
    public Object plugin(Object o) {
        return Plugin.wrap(o, this);
    }

    public <T> T encrypt(Field[] declaredFields, T paramsObject) throws Exception {
        for (Field field : declaredFields) {
            //取出所有被EncryptDecryptField注解的字段
            Encrypted sensitiveField = field.getAnnotation(Encrypted.class);
            if (!Objects.isNull(sensitiveField)) {
                field.setAccessible(true);
                Object object = field.get(paramsObject);
                //暂时只实现String类型的加密
                if (object instanceof String) {
                    String value = (String) object;
                    //加密  这里我使用自定义的AES加密工具
                    field.set(paramsObject, AESUtils.encrypt(value));
                }
            }
        }
        return paramsObject;
    }
}

4.解密拦截器

import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Objects;

/**
 * 解密拦截器
 * @author lima
 * @date 2023/3/6 16:22
 */
@Intercepts({
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
@Slf4j
@Component
public class DecryptInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object resultObject = invocation.proceed();
        try {
            if (Objects.isNull(resultObject)) {
                return null;
            }
            //基于selectList
            if (resultObject instanceof ArrayList) {
                ArrayList resultList = (ArrayList) resultObject;
                if (!CollectionUtils.isEmpty(resultList) && needToDecrypt(resultList.get(0))) {
                    for (Object result : resultList) {
                        //逐一解密
                        decrypt(result);
                    }
                }
                //基于selectOne
            } else {
                if (needToDecrypt(resultObject)) {
                    AESUtils.decrypt((String) resultObject);
                }
            }
            return resultObject;
        } catch (Exception e) {
            log.error("解密失败", e);
        }
        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 o) {
        return Plugin.wrap(o, this);
    }

    public <T> T decrypt(T result) throws Exception {
        //取出resultType的类
        Class<?> resultClass = result.getClass();
        Field[] declaredFields = resultClass.getDeclaredFields();
        for (Field field : declaredFields) {
            //取出所有被EncryptDecryptField注解的字段
            Encrypted sensitiveField = field.getAnnotation(Encrypted.class);
            if (!Objects.isNull(sensitiveField)) {
                field.setAccessible(true);
                Object object = field.get(result);
                //只支持String的解密
                if (object instanceof String) {
                    String value = (String) object;
                    //对注解的字段进行逐一解密
                    field.set(result, AESUtils.decrypt(value));
                }
            }
        }
        return result;
    }
}

5.自定义controller、service、dao等

@RestController
public class UserController {
    @Autowired
    UserService userService;

    @RequestMapping("/create")
    public String create(String name,String password) throws Exception {
        Boolean result = userService.create(name,password);
        if(result){
            return "创建成功";
        }
        return "创建失败";
    }

    @RequestMapping("/query")
    public User query(Long id){
        return userService.query(id);
    }
}
@Service
public class UserServiceImpl implements UserService{
    @Autowired
    UserDao userDao;

    @Override
    public Boolean create(String name, String password) throws Exception {
        return userDao.create(name,1000,password);
    }

    @Override
    public User query(Long id) {
        return userDao.query(id);
    }
}
@Component
public class UserDao {
    @Resource
    UserMapper userMapper;

    public Boolean create(String name, Integer balance, String password) throws Exception {
        User user =new User();
        user.setName(name);
        user.setBalance(balance);
        user.setPassword(password);
        int insert = userMapper.insert(user);
        return insert > 0;
    }

    public User query(Long id) {
        return userMapper.getUserById(id);
    }
}

四.结果演示

1.创建

mybites对字段进行加密解密操作 mybatis plus 字段加密_mysql

这里使用postman模拟请求本地,查看数据库结果 

mybites对字段进行加密解密操作 mybatis plus 字段加密_java_02

2.查询

mybites对字段进行加密解密操作 mybatis plus 字段加密_mybatis_03

业务代码中没有任何关于加解密的代码,但是在插入和查询时,已经自动进行了加解密操作。

以上就是全部内容,希望对你有所帮助。