开发中经常会遇到部分敏感字段需要加密处理后入库的场景,展示时则需要进行解密操作,接下来我们通过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查询时解密失败。