mybatis-plus数据库字段加密处理
在MyBatis Plus中,数据库字段加密处理是一个常见的需求,特别是在处理敏感信息(如手机号、身份证号等)时。保证开发最快、影响范围最小的情况下,我们需要选择一种介于数据库和代码之间的工具来帮我们实现自动加解密
下面文章介绍了使用TypeHandler 自动加密解密,查询更新的时候自动加密进行查询,解决了QueryWrapper 入参加密; UpdateWrapper入参加密;加密字段不支持模糊查询。
文章目录
- mybatis-plus数据库字段加密处理
- 1. 加密方式简介
- 2. TypeHandler对接详解
- 3. 测试
- 3.1. userMapper.insert(user);
- 3.2. userMapper.selectById(1)
- 3.3. userMapper.selectOne()
- 3.4. userMapper.updateById(user)
- 3.5.userMapper.update()
- 3.6. 已解决userMapper.update(new UpdateWrapper())(不成功)
- 3.7. selectBatchIds
- 3.8. selectByMap
- 3.9. selectList
- 3.10. selectMaps(不成功)
- 3.11. selectPage
- 3.12. findAll
- 3.13. findByPhone
- 3.14. updatePhoneById
- 3.15. LambdaQueryWrapper 查询(不成功)
- 3.16. 总结
- 3.17. 增强LambdaUpdateWrapper AbstractLambdaWrapper 解决入参加密失败
- 4. 历史数据加密处理程序
- 5. 参考地址:
1. 加密方式简介
以下是几种常见的加密处理方法,包括使用AES加密、自定义TypeHandler、配置加密以及使用加密解密插件。
1.1. 使用AES加密
AES(Advanced Encryption Standard)是一种广泛使用的对称加密算法。在MyBatis Plus中,可以通过自定义加解密工具类,并在数据持久化和查询时对敏感字段进行加密和解密。
优点:
- 加密强度高,安全性好。
- 加密解密过程相对简单,易于实现。
缺点:
- 需要手动管理加密密钥,存在密钥泄露的风险。
- 加密解密操作会增加一定的性能开销。
实现步骤:
- 定义AES加解密工具类。
- 在数据持久化前对敏感字段进行加密。
- 在数据查询后对敏感字段进行解密。
1.2. 自定义TypeHandler
MyBatis提供了TypeHandler机制,允许用户自定义字段与数据库列之间的映射规则。通过自定义TypeHandler,可以在数据持久化和查询时自动对敏感字段进行加密和解密。
优点:
- 实现了加密解密的自动化,减少了手动操作的繁琐。
- 易于与MyBatis Plus集成,使用简单。
缺点:
- 需要为每个需要加密的字段编写自定义TypeHandler,工作量较大。
- 加密解密逻辑与业务逻辑耦合度较高,不利于维护。
实现步骤:
- 定义加密解密服务接口和实现类。
- 创建自定义TypeHandler,实现加密解密逻辑。
- 在MyBatis配置中注册自定义TypeHandler。
- 在实体类中使用自定义TypeHandler对敏感字段进行映射。
1.3. 配置加密
MyBatis Plus本身并不直接支持配置加密功能,但可以通过一些额外的配置和工具类来实现。例如,可以使用Spring的@Value
注解注入加密后的配置项,并在代码中通过解密工具类进行解密。
优点:
- 可以对配置文件中的敏感信息进行加密,提高安全性。
- 实现相对简单,易于集成。
缺点:
- 加密解密过程需要手动编写,增加了代码复杂度。
- 加密密钥的管理仍然是一个问题。
实现步骤:
- 使用加密工具类对配置文件中的敏感信息进行加密。
- 在Spring配置文件中注入加密后的配置项。
- 在代码中通过解密工具类对配置项进行解密。
1.4. 使用加密解密插件
市面上存在一些针对MyBatis Plus的加密解密插件,如mybatisplus-encrypt-plugin
等。这些插件通过拦截MyBatis的SQL执行过程,对敏感字段进行加密和解密。
优点:
- 实现了加密解密的自动化,无需手动编写加密解密逻辑。
- 插件化设计,易于集成和使用。
缺点:
- 需要引入额外的插件依赖,增加了项目的复杂性。
- 插件的更新和维护需要关注。
实现步骤:
- 引入加密解密插件依赖。
- 配置插件参数,如加密密钥等。
- 在实体类中使用插件提供的注解对敏感字段进行标记。
1.5. 总结
以上几种方法各有优缺点,选择哪种方法取决于具体的使用场景和需求。对于简单的加密需求,可以使用AES加密或自定义TypeHandler;对于需要加密配置文件的场景,可以使用配置加密方法;而对于希望实现自动化加密解密的场景,则可以考虑使用加密解密插件。在实际应用中,应根据项目的具体需求和安全性要求来选择合适的加密处理方法。
由于加密方式可能需要对接第三方的加密,所以采用TypeHandler可以使得一套代码,可以扩展额外的加密方式进行对接。
2. TypeHandler对接详解
2.1. TypeHandler介绍
在 MyBatis 中,类型处理器(TypeHandler)扮演着 JavaType 与 JdbcType 之间转换的桥梁角色。它们用于在执行 SQL 语句时,将 Java 对象的值设置到 PreparedStatement 中,或者从 ResultSet 或 CallableStatement 中取出值。
MyBatis-Plus 给大家提供了提供了一些内置的类型处理器,可以通过 TableField
注解快速注入到 MyBatis 容器中,从而简化开发过程。
2.2. 代码实现
2.2.1. pom配置
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
2.2.2. EncryptProperties
package com.liuhm.encrypt.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @ClassName:EncryptProperties
* @Description: TODO
* @Author: liuhaomin
* @Date: 2024/12/23 15:38
*/
@Data
@Configuration(proxyBeanMethods = false)
@ConfigurationProperties(prefix = "encrypt")
public class EncryptProperties {
/**
* 是否开启加密算法
*/
private Boolean enabled = false;
/**
* 加密算法类
*/
private String algorithmHandler = "com.liuhm.encrypt.handler.Base64EncryptHandler";
}
2.2.3. 加密解密接口
package com.liuhm.encrypt.handler;
/**
* @ClassName:EncryptHandler
* @Description: 加密类
* @Author: liuhaomin
* @Date: 2024/12/23 14:06
*/
public interface EncryptHandler {
/**
* 加密算法
* @param content
* @return
*/
String encrypt(String content);
/**
* 解密算法
* @param content
* @return
*/
String decrypt(String content);
}
2.2.4. 加密额解密实现
- DESEncryptHandler
package com.liuhm.encrypt.handler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.SecureRandom;
import java.util.Base64;
/**
* @ClassName:DESEncryptHandler
* @Description: TODO
* @Author: liuhaomin
* @Date: 2024/12/23 15:25
*/
@Slf4j
public class DESEncryptHandler implements EncryptHandler {
public static final String DESKEY = "liuhm123";
/**
* 解密
*
* @param data
*
* @return
*/
@Override
public String decrypt(String data) {
return decrypt(data, DESKEY);
}
/**
* 加密
*
* @param data
*
* @return
*/
@Override
public String encrypt(String data) {
return encrypt(data, DESKEY);
}
/**
* 加密
*
* @param data
* @param key
*
* @return
*/
public static String encrypt(String data, String key) {
if (StringUtils.isEmpty(data) || StringUtils.isEmpty(key)) {
log.info("参数和密钥不允许为空");
return null;
}
String strs = null;
try {
byte[] bytes = encryptOrDecrypt(Cipher.ENCRYPT_MODE, data.getBytes(), key.getBytes());
// base64编码字节
strs = Base64.getEncoder().encodeToString(bytes);
} catch (Exception e) {
log.error("加密失败,errormsg={}", e.getMessage());
}
return strs;
}
/**
* 解密
*
* @param data
* @param key
*
* @return
*/
public static String decrypt(String data, String key) {
if (StringUtils.isEmpty(data) || StringUtils.isEmpty(key)) {
log.info("参数和密钥不允许为空");
return null;
}
String strs = null;
try {
byte[] src = Base64.getDecoder().decode(data);
byte[] bytes = encryptOrDecrypt(Cipher.DECRYPT_MODE, src, key.getBytes());
strs = new String(bytes, "utf-8");
} catch (UnsupportedEncodingException e) {
log.error("解密失败,errormsg={}", e.getMessage());
}
return strs;
}
public static byte[] encryptOrDecrypt(int mode, byte[] data, byte[] key) {
try {
// 强随机数生成器 (RNG)
SecureRandom random = new SecureRandom();
// DESKeySpec是一个成加密密钥的密钥内容的(透明)规范的接口。
DESKeySpec desKey = new DESKeySpec(key);
// 创建一个密匙工厂,然后用它把DESKeySpec转换成
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
// 得到密钥对象SecretKey
SecretKey securekey = keyFactory.generateSecret(desKey);
// Cipher对象实际完成加密操作
Cipher cipher = Cipher.getInstance("DES");
// 用密匙初始化Cipher对象
cipher.init(mode, securekey, random);
// 现在,获取数据并加密,正式执行加密操作
return cipher.doFinal(data);
} catch (Throwable e) {
log.error("{}", e);
}
return null;
}
public static void main(String[] args) {
DESEncryptHandler handler = new DESEncryptHandler();
System.out.println(handler.encrypt("123456"));
System.out.println(handler.decrypt(handler.encrypt("123456")));
}
}
- Base64EncryptHandler
package com.liuhm.encrypt.handler;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* @ClassName:Base64EncryptHandler
* @Description: base64加密解密
* @Author: liuhaomin
* @Date: 2024/12/23 15:20
*/
public class Base64EncryptHandler implements EncryptHandler {
@Override
public String encrypt(String content) {
try {
return Base64.getEncoder().encodeToString(content.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
throw new RuntimeException("encrypt fail!", e);
}
}
@Override
public String decrypt(String content) {
try {
byte[] asBytes = Base64.getDecoder().decode(content);
return new String(asBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("decrypt fail!", e);
}
}
public static void main(String[] args) {
Base64EncryptHandler handler = new Base64EncryptHandler();
System.out.println(handler.encrypt("123456"));
System.out.println(handler.decrypt(handler.encrypt("123456")));
}
}
- AESEncryptHandler
package com.liuhm.encrypt.handler;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import lombok.extern.slf4j.Slf4j;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* @ClassName:AESEncryptHandler
* @Description: TODO
* @Author: liuhaomin
* @Date: 2024/12/23 15:22
*/
@Slf4j
public class AESEncryptHandler implements EncryptHandler {
/**
* aes算法需要秘钥key
*/
private String key = "e247d43f717440a1";
/**
* aes算法需要一个偏移量
* AES算法的偏移量长度必须为16字节(128位)
*/
private String aesIv = "9491a86cb4574440";
@Override
public String encrypt(String content) {
try {
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), Constants.AES);
byte[] enCodeFormat = secretKey.getEncoded();
SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, Constants.AES);
IvParameterSpec iv = new IvParameterSpec(aesIv.getBytes(StandardCharsets.UTF_8));
Cipher cipher = Cipher.getInstance(Constants.AES_CBC_CIPHER);
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, iv);
byte[] valueByte = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(valueByte);
} catch (Exception e) {
log.error("加密失败:", e);
throw new RuntimeException("encrypt fail!", e);
}
}
@Override
public String decrypt(String content) {
try {
byte[] originalData = Base64.getDecoder().decode(content.getBytes(StandardCharsets.UTF_8));
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), Constants.AES);
byte[] enCodeFormat = secretKey.getEncoded();
SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, Constants.AES);
IvParameterSpec iv = new IvParameterSpec(aesIv.getBytes(StandardCharsets.UTF_8));
Cipher cipher = Cipher.getInstance(Constants.AES_CBC_CIPHER);
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, iv);
byte[] valueByte = cipher.doFinal(originalData);
return new String(valueByte);
} catch (Exception e) {
log.error("解密失败:", e);
throw new RuntimeException("decrypt fail!", e);
}
}
public static void main(String[] args) {
AESEncryptHandler handler = new AESEncryptHandler();
System.out.println(handler.encrypt("123456"));
System.out.println(handler.decrypt(handler.encrypt("123456")));
}
}
2.2.5. 实现数据库的字段保存加密与查询解密处理类
EncryptTypeHandler
package com.liuhm.encrypt;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.liuhm.encrypt.handler.EncryptHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.slf4j.MDC;
import javax.annotation.Resource;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Objects;
/**
* @ClassName:EncryptTypeHandler
* @Description: EncryptTypeHandler
* @Author: liuhaomin
* @Date: 2024/12/23 14:06
*/
@Slf4j
public class EncryptTypeHandler<T> extends BaseTypeHandler<T> {
private static final String ENCRYPTTYPE = "ENCRYPTTYPE";
/**
* 增加忽略
*/
public static void ignoreEncrypt(){
MDC.put(ENCRYPTTYPE, "1");
}
/**
* 清理忽略
*/
public static void cleanIgnore(){
MDC.remove(ENCRYPTTYPE);
}
/**
* 检查是否忽略
* @return
*/
private Boolean checkIgnore(){
if(MDC.get(ENCRYPTTYPE) == null){
return false;
}
return true;
}
@Resource
private EncryptHandler encryptHandler;
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, encrypt((String)parameter));
}
@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
String columnValue = rs.getString(columnName);
//有一些可能是空字符
return StringUtils.isBlank(columnValue) ? (T)columnValue : (T)decrypt(columnValue);
}
@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String columnValue = rs.getString(columnIndex);
return StringUtils.isBlank(columnValue) ? (T)columnValue : (T)decrypt(columnValue);
}
@Override
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String columnValue = cs.getString(columnIndex);
return StringUtils.isBlank(columnValue) ? (T)columnValue : (T)decrypt(columnValue);
}
// 替换为你的加密和解密方法
private String encrypt(String text) {
if(Objects.isNull(encryptHandler) || StringUtils.isBlank(text)){
return text;
}
if(checkIgnore()){
return text;
}
// 实现加密逻辑
// 假设返回加密后的字符串
// return Base64.getEncoder().encodeToString(text.getBytes());
return encryptHandler.encrypt(text);
}
private String decrypt(String text) {
if(Objects.isNull(encryptHandler) || StringUtils.isBlank(text)){
return text;
}
if(checkIgnore()){
return text;
}
// 实现解密逻辑
// 假设返回解密后的字符串
// byte[] decode = Base64.getDecoder().decode(text);
// return new String(decode, StandardCharsets.UTF_8);
return encryptHandler.decrypt(text);
}
}
2.2.6. 加密解密实现配置
EncryptAutoConfiguration
package com.liuhm.encrypt.config;
import com.liuhm.encrypt.handler.EncryptHandler;
import com.liuhm.encrypt.properties.EncryptProperties;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
/**
* mybatis 自动配置类
*
* @author liuhaomin
* @date 2023.11.16
*/
@Configuration
@MapperScan("com.liuhm.dao.mapper*")
@EnableConfigurationProperties({EncryptProperties.class})
// false 不注入对应的加密处理
public class EncryptAutoConfiguration {
@Bean
@ConditionalOnProperty(name = "encrypt.enabled", havingValue = "true", matchIfMissing = false)
public EncryptTypeHandler encryptTypeHandler() {
return new EncryptTypeHandler();
}
@Resource
private EncryptProperties encryptProperties;
@Bean
@ConditionalOnMissingBean(EncryptHandler.class)
@ConditionalOnProperty(name = "encrypt.enabled", havingValue = "true", matchIfMissing = false)
public EncryptHandler encryptHandler() {
String algorithmHandler = encryptProperties.getAlgorithmHandler();
Class<?> algorithmHandlerClass;
EncryptHandler encryptHandler = null;
try {
algorithmHandlerClass = Class.forName(algorithmHandler);
// 使用无参数构造函数创建对象
encryptHandler = (EncryptHandler) algorithmHandlerClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
return encryptHandler;
}
}
2.2.7. 创建实体类
- 在实体类上加上
@TableName(value = "表名", autoResultMap = true)
- 在需要加密的属性上加上
@TableField(value = "字段", typeHandler = EncryptTypeHandler.class)
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "user", autoResultMap = true)
public class User implements Serializable {
private static final long serialVersionUID = -1L;
private Integer id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
@TableField(typeHandler = EncryptTypeHandler.class)
private String password;
/**
* 电话号码
*/
@TableField(typeHandler = EncryptTypeHandler.class)
private String phone;
}
2.2.8. 配置定义使用的加密类型
encrypt:
enabled: true
algorithmHandler: com.liuhm.encrypt.handler.Base64EncryptHandler
2.2.9. 自定义mapper sql
UserMapper.java
@Repository
@CacheNamespace
public interface UserMapper extends BaseMapper<User> {
List<User> findAll();
void updatePhoneById(@Param("phone") String phone, @Param("id") Integer id);
List<User> findByPhone(@Param("phone") String phone);
}
UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.liuhm.dao.mapper.UserMapper">
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.liuhm.entity.User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password" typeHandler="com.liuhm.encrypt.EncryptTypeHandler"/>
<result column="phone" property="phone" typeHandler="com.liuhm.encrypt.EncryptTypeHandler"/>
</resultMap>
<!-- 查询全部 -->
<select id="findAll" resultMap="BaseResultMap">
SELECT * FROM user
</select>
<!-- 更新手机号 -->
<update id="updatePhoneById">
update user set phone = #{phone, typeHandler=com.liuhm.encrypt.EncryptTypeHandler} where id = #{id}
</update>
<!-- 根据手机号查询 -->
<select id="findByPhone" resultMap="BaseResultMap">
SELECT * FROM user where phone = #{phone, typeHandler=com.liuhm.encrypt.EncryptTypeHandler}
</select>
</mapper>
2.2.10. mysql脚本
drop table if exists user;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`username` varchar(255) DEFAULT NULL COMMENT '名字',
`password` varchar(255) DEFAULT NULL COMMENT '密码',
`phone` varchar(255) DEFAULT NULL COMMENT '电话号码',
PRIMARY KEY (`id`)
) COMMENT='用户表';
3. 测试
所有的单元测试在UserMapperTest.java
3.1. userMapper.insert(user);
@Test
public void insert(){
User user = new User();
user.setUsername("admin");
user.setPassword("123456");
user.setPhone("15723219655");
userMapper.insert(user);
}
3.2. userMapper.selectById(1)
@Test
public void selectById(){
User user = userMapper.selectById(1);
log.info("selectById user:{}",user);
}
3.3. userMapper.selectOne()
@Test
public void selectOne(){
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getId, 1));
log.info("selectOne user:{}",user);
}
3.4. userMapper.updateById(user)
@Test
public void updateById(){
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getId, 1));
user.setPassword("654321");
userMapper.updateById(user);
user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getId, 1));
log.info("updateById user:{}",user);
}
3.5.userMapper.update()
@Test
public void update(){
User user = new User();
user.setPassword("123456789");
userMapper.update(user,new LambdaQueryWrapper<User>().eq(User::getId, 1));
user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getId, 1));
log.info("update user:{}",user);
}
3.6. 已解决userMapper.update(new UpdateWrapper())(不成功)
lambda
@Test
public void update2(){
userMapper.update(null,new UpdateWrapper<User>().lambda()
.set(User::getPassword, "123456")
.eq(User::getId, 1));
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getId, 1));
log.info("update2 user:{}",user);
}
解决办法:
@Test
public void update3() {
userMapper.update(null,new UpdateWrapper<User>().lambda()
.set(User::getPassword, "123456","typeHandler=com.liuhm.encrypt.EncryptTypeHandler")
.eq(User::getId, 1));
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getId, 1));
log.info("update3 user:{}",user);
}
非lambda
@Test
public void update4() {
userMapper.update(null,new UpdateWrapper<User>().set("phone", "13888888888")
.eq("id", "1"));
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getId, 1));
log.info("update4 user:{}",user);
}
解决办法:
@Test
public void update5() {
userMapper.update(null,new UpdateWrapper<User>().set("phone", "13888888888","typeHandler=com.liuhm.encrypt.EncryptTypeHandler")
.eq("id", "1"));
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getId, 1));
log.info("update5 user:{}",user);
}
3.7. selectBatchIds
@Test
public void selectBatchIds(){
List<User> user = userMapper.selectBatchIds(Arrays.asList("1", "2", "3"));
log.info("selectBatchIds user:{}",user);
}
3.8. selectByMap
@Test
public void selectByMap(){
List<User> user = userMapper.selectByMap(null);
log.info("selectByMap user:{}",user);
}
3.9. selectList
@Test
public void selectList(){
List<User> user = userMapper.selectList(null);
log.info("selectList user:{}",user);
}
3.10. selectMaps(不成功)
@Test
public void selectMaps(){
List<Map<String,Object>> user = userMapper.selectMaps(null);
log.info("selectMaps user:{}",user);
}
3.11. selectPage
@Test
public void selectPage(){
IPage<User> user = userMapper.selectPage(new Page<User>(1,5),null);
log.info("selectPage user:{}",user.getRecords());
}
3.12. findAll
List<User> findAll();
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.liuhm.entity.User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password" typeHandler="com.liuhm.encrypt.EncryptTypeHandler"/>
<result column="phone" property="phone" typeHandler="com.liuhm.encrypt.EncryptTypeHandler"/>
</resultMap>
<!-- 查询全部 -->
<select id="findAll" resultMap="BaseResultMap">
SELECT * FROM user
</select>
@Test
public void findAll(){
List<User> user = userMapper.findAll();
log.info("findAll user:{}",user);
}
3.13. findByPhone
List<User> findByPhone(@Param("phone") String phone)
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.liuhm.entity.User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password" typeHandler="com.liuhm.encrypt.EncryptTypeHandler"/>
<result column="phone" property="phone" typeHandler="com.liuhm.encrypt.EncryptTypeHandler"/>
</resultMap>
<!-- 根据手机号查询 -->
<select id="findByPhone" resultMap="BaseResultMap">
SELECT * FROM user where phone = #{phone, typeHandler=com.liuhm.encrypt.EncryptTypeHandler}
</select>
@Test
public void findByPhone(){
List<User> user = userMapper.findByPhone("15723219655");
log.info("findByPhone user:{}",user);
}
3.14. updatePhoneById
void updatePhoneById(@Param("phone") String phone, @Param("id") Integer id);
<!-- 更新手机号 -->
<update id="updatePhoneById">
update user set phone = #{phone, typeHandler=com.liuhm.encrypt.EncryptTypeHandler} where id = #{id}
</update>
@Test
public void updatePhoneById(){
userMapper.updatePhoneById("15723219651",1);
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getId, 1));
log.info("updatePhoneById user:{}",user);
}
3.15. LambdaQueryWrapper 查询(不成功)
@Test
public void lambdaQueryWrapper (){
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, "15723219655"));
log.info("lambdaQueryWrapper user:{}",user);
}
解决办法:
手动加密
不支持like查询
@Autowired
EncryptHandler encryptHandler;
@Test
public void lambdaQueryWrapper2 (){
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, encryptHandler.encrypt("15723219655")));
log.info("lambdaQueryWrapper2 user:{}",user);
}
3.16. 总结
由于测试内容较多,这里先直接展示测试结果
操作 | 实现方式 | 入参 | 测试结果 |
---|---|---|---|
SELECT | 原生SQL | 非加密字段 | 出参解密成功 |
SELECT | QueryWrapper | 非加密字段 | 出参解密成功 |
SELECT | 原生SQL | 加密字段 | 入参加密成功 |
SELECT | QueryWrapper | 加密字段 | 入参加密失败(手动加密) |
UPDATE | 原生SQL | 加密字段 | 入参加密成功 |
UPDATE | UpdateWrapper | 加密字段 | 入参加密成功 |
UPDATE | LambdaUpdateWrapper | 加密字段 | 入参加密成功 |
UPDATE | updateById | 加密字段 | 入参加密成功 |
INSERT | Service | 加密字段 | 入参加密成功 |
MyBatis Plus 缺陷
QueryWrapper 不支持入参加密(手动加密);
加密字段不支持模糊查询。
3.17. 增强LambdaUpdateWrapper AbstractLambdaWrapper 解决入参加密失败
- 增强LambdaUpdateWrapper AbstractLambdaWrapper 使得查询对应字段自动加密,更新字段自动加密
- 代码里面所有都是 new QueryWrapper().lambda() new LambdaQueryWrapper()
- 代码里面所有都是new UpdateWrapper().lambda() new LambdaUpdateWrapper()
3.17.1. 改造目录
3.17.2. 改造代码
- 复制AbstractLambdaWrapper 重写下面的方法
@Override
public Children between(boolean condition, SFunction<T, ?> column, Object val1, Object val2) {
// 增强mapping
String mapping = getMapping(column);
return maybeDo(condition, () -> appendSqlSegments(columnToSqlSegment(column), BETWEEN,
() -> formatParam(mapping, val1), AND, () -> formatParam(mapping, val2)));
}
@Override
public Children notBetween(boolean condition,SFunction<T, ?> column, Object val1, Object val2) {
// 增强mapping
String mapping = getMapping(column);
return maybeDo(condition, () -> appendSqlSegments(columnToSqlSegment(column), NOT_BETWEEN,
() -> formatParam(mapping, val1), AND, () -> formatParam(mapping, val2)));
}
@Override
protected Children likeValue(boolean condition, SqlKeyword keyword, SFunction<T, ?> column, Object val, SqlLike sqlLike) {
// 增强mapping
String mapping = getMapping(column);
return maybeDo(condition, () -> appendSqlSegments(columnToSqlSegment(column), keyword,
() -> formatParam(mapping, SqlUtils.concatLike(val, sqlLike))));
}
@Override
protected Children addCondition(boolean condition, SFunction<T, ?> column, SqlKeyword sqlKeyword, Object val) {
// 增强mapping
String mapping = getMapping(column);
return maybeDo(condition, () -> appendSqlSegments(columnToSqlSegment(column), sqlKeyword,
() -> formatParam(mapping, val)));
}
@Override
public Children in(boolean condition, SFunction<T, ?> column, Collection<?> coll) {
return maybeDo(condition, () -> appendSqlSegments(columnToSqlSegment(column), IN, inExpression(coll,column)));
}
@Override
public Children in(boolean condition, SFunction<T, ?> column, Object... values) {
return maybeDo(condition, () -> appendSqlSegments(columnToSqlSegment(column), IN, inExpression(values,column)));
}
@Override
public Children notIn(boolean condition, SFunction<T, ?> column, Collection<?> coll) {
return maybeDo(condition, () -> appendSqlSegments(columnToSqlSegment(column), NOT_IN, inExpression(coll,column)));
}
@Override
public Children notIn(boolean condition, SFunction<T, ?> column, Object... values) {
return maybeDo(condition, () -> appendSqlSegments(columnToSqlSegment(column), NOT_IN, inExpression(values,column)));
}
public ISqlSegment inExpression(Collection<?> value,SFunction<T, ?> column) {
if (CollectionUtils.isEmpty(value)) {
return () -> "()";
}
// 增强mapping
String mapping = getMapping(column);
return () -> value.stream().map(i -> formatParam(mapping, i))
.collect(joining(StringPool.COMMA, StringPool.LEFT_BRACKET, StringPool.RIGHT_BRACKET));
}
/**
* 获取in表达式 包含括号
*
* @param values 数组
*/
public ISqlSegment inExpression(Object[] values,SFunction<T, ?> column) {
if (ArrayUtils.isEmpty(values)) {
return () -> "()";
}
// 增强mapping
String mapping = getMapping(column);
return () -> Arrays.stream(values).map(i -> formatParam(mapping, i))
.collect(joining(StringPool.COMMA, StringPool.LEFT_BRACKET, StringPool.RIGHT_BRACKET));
}
public String getMapping( SFunction<T, ?> column){
// 增强mapping
ColumnCache cache = getColumnCache(column);
return cache.getMapping();
}
-
复制LambdaUpdateWrapper
重新下面的方法
@Override public LambdaUpdateWrapper<T> set(boolean condition, SFunction<T, ?> column, Object val, String mapping) { if(StringUtils.isBlank(mapping)) { mapping = getMapping(column); } String finalMapping = mapping; return maybeDo(condition, () -> { String sql = formatParam(finalMapping, val); sqlSet.add(columnToString(column) + Constants.EQUALS + sql); }); }
3.17.3.测试
测试1. 成功
@Test
public void lambdaQueryWrapperNew (){
User user = userMapper.selectOne(new QueryWrapper<User>().lambda().eq(User::getPhone, "15723219655"));
log.info("lambdaQueryWrapperNew user:{}",user);
user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, "15723219655"));
log.info("lambdaQueryWrapperNew user:{}",user);
List<User> users = userMapper.selectList(new LambdaQueryWrapper<User>().in(User::getPhone, Arrays.asList("15723219655", "15723219654")));
log.info("lambdaQueryWrapperNew users:{}",users);
}
测试2. 成功
@Test
public void updatenew() {
userMapper.update(null,new UpdateWrapper<User>().lambda()
.set(User::getPassword, "123456")
.eq(User::getId, 1));
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getId, 1));
log.info("updatenew user:{}",user);
}
4. 历史数据加密处理程序
@Test
public void encryptUser() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 加密 用户信息
Long count = userMapper.selectCount(null);
int pageSize = 1000;
Long pageCount = count / pageSize + 1;
// 必须用唯一且非空字段进行排序,否则 pageHelper 查出来的数据可能会有重复。
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<User>().orderByAsc(User::getId);
for (int i = 0; i < pageCount; i++) {
log.info(">>>>>>>>>> 【INFO】加密用户信息,当前页数:{},总页数:{}", i + 1, pageCount);
Page page = new Page(i, pageSize);
EncryptTypeHandler.ignoreEncrypt();
IPage<User> iPage = userMapper.selectPage(page,queryWrapper);
List<User> users = iPage.getRecords();
List<User> userList = new ArrayList<>();
users.stream().forEach(o -> {
try {
User user = objectMapper.readValue(objectMapper.writeValueAsString(o), User.class);
userList.add(user);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
});
EncryptTypeHandler.cleanIgnore();
userService.updateBatchById(userList);
}
}
5. 参考地址:
- MyBatis Plus 数据库字段加密处理,https://blog.csdn.net/qq_33204709/article/details/129178188
- MyBatis-Plus 优雅实现数据加密存储 https://blog.csdn.net/xiaohuihui1400/article/details/135986224
- mybatis plus 官方问题页面,https://github.com/baomidou/mybatis-plus/issues
- 更新时自定义的TypeHandler不生效,https://github.com/baomidou/mybatis-plus/issues/794
- lambdaUpdate() 无法更新Json对象字段,https://github.com/baomidou/mybatis-plus/issues/5031
- LambdaUpdateWrapper不支持自定义BaseTypeHandler,https://github.com/baomidou/mybatis-plus/issues/3317
下面的mybatis-plus-encrypt