MENU

Mybatis TypeHandler 实现敏感字段加密

July 28, 2022 • 技术阅读设置

开发中经常会遇到部分敏感字段需要加密处理后入库的场景,展示时则需要进行解密操作,接下来我们通过Mybatis的TypeHandler来解决。

步骤

1.创建 AESUtil 加解密工具类。
2.创建 CryptoTypeHandler 处理器。
3.在需要加解密的实体中添加注解配置。

代码

AESUtil

package com.example.practise.utils;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

/**
 * AES 加解密工具类
 *
 * @author LenonJin
 * @date 2022-07-28
 */

@Slf4j
public class AesUtil {
    private final static int KEY_SIZE = 128;
    private final static String ALGORITHM_AES = "AES";
    private final static String ALGORITHM_AES_PKCS5 = "AES/ECB/PKCS5Padding";

    /**
     * 生成AES密钥,base64编码格式 (128)
     *
     * @return
     * @throws Exception
     */
    public static String getKeyAES_128() throws Exception {
        KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM_AES_PKCS5);
        keyGen.init(KEY_SIZE);
        SecretKey key = keyGen.generateKey();
        String base64str = Base64.encodeBase64String(key.getEncoded());
        return base64str;
    }

    /**
     * 根据base64Key获取SecretKey对象
     *
     * @param base64Key
     * @return
     */
    public static SecretKey loadKeyAES(String base64Key) {
        byte[] bytes = Base64.decodeBase64(base64Key);
        SecretKeySpec secretKeySpec = new SecretKeySpec(bytes, ALGORITHM_AES);
        return secretKeySpec;
    }

    /**
     * AES 加密字符串
     *
     * @param base64Key
     * @param encryptData
     * @param encode
     * @return
     */
    public static String encrypt(String base64Key, String encryptData, String encode) {
        SecretKey key = loadKeyAES(base64Key);
        try {
            final Cipher cipher = Cipher.getInstance(ALGORITHM_AES_PKCS5);
            cipher.init(Cipher.ENCRYPT_MODE, key);
            byte[] encryptBytes = encryptData.getBytes(encode);
            byte[] result = cipher.doFinal(encryptBytes);
            return Base64.encodeBase64String(result);
        } catch (Exception e) {
            log.error("加密异常:" + e.getMessage());
            return null;
        }
    }

    /**
     * AES 解密字符串
     *
     * @param base64Key
     * @param decryptData
     * @param encode
     * @return
     */
    public static String decrypt(String base64Key, String decryptData, String encode) {
        SecretKey key = loadKeyAES(base64Key);
        try {
            final Cipher cipher = Cipher.getInstance(ALGORITHM_AES_PKCS5);
            cipher.init(Cipher.DECRYPT_MODE, key);
            byte[] decryptBytes = Base64.decodeBase64(decryptData);
            byte[] result = cipher.doFinal(decryptBytes);
            return new String(result, encode);
        } catch (Exception e) {
            log.error("解密异常:" + e.getMessage());
            return null;
        }
    }

}

CryptoTypeHandler

package com.example.practise.handler;

import com.example.practise.utils.AesUtil;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;

import java.sql.*;

/**
 * 自定义类型处理器:基于Mybatis-Plus的字段加密
 * 使用方法:
 * 方式一:数据实体添加注解 @TableName( value="sys_dept",autoResultMap=true),属性添加注解 @TableField(value = "secret_info", typeHandler = CryptoTypeHandler.class)
 * 方式二:mapper.xml文件 <result property="phonenumber" column="phonenumber" typeHandler="com.zkrh.common.handler.CryptoTypeHandler"/>
 *
 * @author LenonJin
 * @date 2022-07-28
 */
public class CryptoTypeHandler implements TypeHandler<String> {

    /**
     * 加解密密钥,长度为24位
     * Todo (不建议直接写在类中,可以通过环境变量进行读取)
     */
    private final static String SALT = "fR3eF/VH91SuZ+SWxF21Kg==";
    private final static String CHARSET_NAME = "UTF-8";

    @Override
    public void setParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        if (parameter != null) {
            String encryptedText = AesUtil.encrypt(SALT, parameter, CHARSET_NAME);
            ps.setString(i, encryptedText);
        } else {
            ps.setNull(i, Types.VARCHAR);
        }
    }

    @Override
    public String getResult(ResultSet rs, String columnName) throws SQLException {
        String result = rs.getString(columnName);
        if (result == null) {
            return null;
        }

        String decryptedText = AesUtil.decrypt(SALT, result, CHARSET_NAME);
        return decryptedText;
    }

    @Override
    public String getResult(ResultSet rs, int columnIndex) throws SQLException {
        String result = rs.getString(columnIndex);
        if (result == null) {
            return null;
        }

        String decryptedText = AesUtil.decrypt(SALT, result, CHARSET_NAME);
        return decryptedText;
    }

    @Override
    public String getResult(CallableStatement cs, int columnIndex) throws SQLException {
        String result = cs.getString(columnIndex);
        if (result == null) {
            return null;
        }

        String decryptedText = AesUtil.decrypt(SALT, result, CHARSET_NAME);
        return decryptedText;
    }
}

注解使用

测试实体 User

在需要加密的实体添加注解 @TableName(value = "user", autoResultMap = true)
在需要加密的字段添加注解 @TableField(value = "email", typeHandler = CryptoTypeHandler.class)

package com.example.practise.entity;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.example.practise.handler.CryptoTypeHandler;
import lombok.Data;

/**
 * description: mybatis TypeHandler 加密测试-实体
 * date: 2023/6/25 14:59
 * author: LenonJin
 */
@Data
@TableName(value = "user", autoResultMap = true)
public class User {
    /**
     * 主键ID
     */
    private Long id;

    /**
     * 名称
     */
    private String name;

    /**
     * 年龄
     */
    private Integer age;

    /**
     * 邮箱(加密)
     */
    @TableField(value = "email", typeHandler = CryptoTypeHandler.class)
    private String email;

    /**
     * 电话(加密)
     */
    @TableField(value = "phone", typeHandler = CryptoTypeHandler.class)
    private String phone;
}

测试接口 UserController

package com.example.practise.controller;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.practise.annotation.WebLog;
import com.example.practise.entity.User;
import com.example.practise.entity.UserMapper;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.List;

/**
 * description: mybatis TypeHandler 加密测试
 * date: 2023/6/25 14:29
 * author: LenonJin
 */
@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    UserMapper userMapper;

    /**
     * test add
     */
    @WebLog(description = "测试新增用户")
    @PostMapping("/add")
    public Boolean add(@RequestBody User user) {

        return userMapper.insert(user) > 0;
    }

    /**
     * test select
     */
    @WebLog(description = "测试查询用户")
    @GetMapping("/select")
    public List<User> select() {
        return userMapper.selectList(new QueryWrapper<User>());
    }
}

测试结果

从新增接口的sql日志中可以看出,新增数据的email及phone字段已进行了加密入库。
从查询接口的返回日志中可以看出,加密的eamil及phone字段已进行了解密返回。

2023-06-25 16:45:49.023  INFO 7968 --- [nio-8080-exec-1] c.example.practise.aspectj.WebLogAspect  : ----------------------------------------------------------- Start -----------------------------------------------------------
2023-06-25 16:45:49.084  INFO 7968 --- [nio-8080-exec-1] c.example.practise.aspectj.WebLogAspect  : Request Uri    :/user/add
2023-06-25 16:45:49.085  INFO 7968 --- [nio-8080-exec-1] c.example.practise.aspectj.WebLogAspect  : Request Ip     :127.0.0.1
2023-06-25 16:45:49.085  INFO 7968 --- [nio-8080-exec-1] c.example.practise.aspectj.WebLogAspect  : Http Method    :POST
2023-06-25 16:45:49.085  INFO 7968 --- [nio-8080-exec-1] c.example.practise.aspectj.WebLogAspect  : Description    :测试新增用户
2023-06-25 16:45:49.087  INFO 7968 --- [nio-8080-exec-1] c.example.practise.aspectj.WebLogAspect  : Class Method   :com.example.practise.controller.UserController.add
2023-06-25 16:45:49.087  INFO 7968 --- [nio-8080-exec-1] c.example.practise.aspectj.WebLogAspect  : Request Args   :[{"id":10011,"name":"孙悟空","age":150,"email":"sunwukong@qq.com","phone":"13019199191"}]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@65ff4bcb] was not registered for synchronization because synchronization is not active
2023-06-25 16:45:49.123  INFO 7968 --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2023-06-25 16:45:49.351  INFO 7968 --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@488841813 wrapping com.mysql.cj.jdbc.ConnectionImpl@7abd7e2a] will not be managed by Spring
==>  Preparing: INSERT INTO user ( id, name, age, email, phone ) VALUES ( ?, ?, ?, ?, ? )
==> Parameters: 10011(Long), 孙悟空(String), 150(Integer), rmEJZVvz8/Dwv/iQOQGBpKsvd8iUD5gzfj4KmaFCCOg=(String), JmwYpZdNTa0DIqbDXN7mrg==(String)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@65ff4bcb]
2023-06-25 16:45:50.199  INFO 7968 --- [nio-8080-exec-1] c.example.practise.aspectj.WebLogAspect  : Response Args  :{}
2023-06-25 16:45:50.199  INFO 7968 --- [nio-8080-exec-1] c.example.practise.aspectj.WebLogAspect  : Time Consuming :1176 ms
2023-06-25 16:45:50.199  INFO 7968 --- [nio-8080-exec-1] c.example.practise.aspectj.WebLogAspect  : ------------------------------------------------------------ End ------------------------------------------------------------
2023-06-25 16:45:53.333  INFO 7968 --- [nio-8080-exec-2] c.example.practise.aspectj.WebLogAspect  : ----------------------------------------------------------- Start -----------------------------------------------------------
2023-06-25 16:45:53.334  INFO 7968 --- [nio-8080-exec-2] c.example.practise.aspectj.WebLogAspect  : Request Uri    :/user/select
2023-06-25 16:45:53.334  INFO 7968 --- [nio-8080-exec-2] c.example.practise.aspectj.WebLogAspect  : Request Ip     :127.0.0.1
2023-06-25 16:45:53.334  INFO 7968 --- [nio-8080-exec-2] c.example.practise.aspectj.WebLogAspect  : Http Method    :GET
2023-06-25 16:45:53.334  INFO 7968 --- [nio-8080-exec-2] c.example.practise.aspectj.WebLogAspect  : Description    :测试查询用户
2023-06-25 16:45:53.334  INFO 7968 --- [nio-8080-exec-2] c.example.practise.aspectj.WebLogAspect  : Class Method   :com.example.practise.controller.UserController.select
2023-06-25 16:45:53.334  INFO 7968 --- [nio-8080-exec-2] c.example.practise.aspectj.WebLogAspect  : Request Args   :args is null.
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3c5069a] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1189734531 wrapping com.mysql.cj.jdbc.ConnectionImpl@7abd7e2a] will not be managed by Spring
==>  Preparing: SELECT id,name,age,email,phone FROM user
==> Parameters: 
<==    Columns: id, name, age, email, phone
<==        Row: 10011, 孙悟空, 150, rmEJZVvz8/Dwv/iQOQGBpKsvd8iUD5gzfj4KmaFCCOg=, JmwYpZdNTa0DIqbDXN7mrg==
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3c5069a]
2023-06-25 16:45:53.371  INFO 7968 --- [nio-8080-exec-2] c.example.practise.aspectj.WebLogAspect  : Response Args  :[{"id":10011,"name":"孙悟空","age":150,"email":"sunwukong@qq.com","phone":"13019199191"}]
2023-06-25 16:45:53.371  INFO 7968 --- [nio-8080-exec-2] c.example.practise.aspectj.WebLogAspect  : Time Consuming :38 ms
2023-06-25 16:45:53.371  INFO 7968 --- [nio-8080-exec-2] c.example.practise.aspectj.WebLogAspect  : ------------------------------------------------------------ End ------------------------------------------------------------

拓展

SQL查询:
如何使用sql解密数据:select (aes_decrypt(from_base64(email),from_base64('fR3eF/VH91SuZ+SWxF21Kg=='))) as email,'name' from `user`;

CryptoTypeHandler:
代码private final static String SALT = "fR3eF/VH91SuZ+SWxF21Kg==";加密的盐不建议直接写在类中,可通过环境变量等方式读取提高安全性。且长度为24位,不然会导致sql查询时解密失败。

Last Modified: November 17, 2023