1.場景介紹
- 當(dāng)項目開發(fā)到一半,可能突然客戶會要求對數(shù)據(jù)庫里面比如手機號、身份證號的字段進(jìn)行加密;
- 在保證開發(fā)最快、影響范圍最小的情況下,我們需要選擇一種介于數(shù)據(jù)庫和代碼之間的工具來幫我們實現(xiàn)自動加解密;
2.Maven依賴
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<!-- mybatis的分頁插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.11</version>
<!-- pagehelper 包含該依賴存在版本沖突,因此不建議和 mp 一起混用 -->
<exclusions>
<exclusion>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
</exclusion>
</exclusions>
</dependency>
2.AESUtil.java 加解密工具類
這里我們選用AES對稱加密算法,因為它是可逆算法。
AES加密介紹: https://blog.csdn.net/qq_33204709/article/details/126930720
具體實現(xiàn)代碼如下:
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Base64Utils;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* AES加密工具類
*
* @author ACGkaka
* @since 2021-06-18 19:11:03
*/
public class AESUtil {
/**
* 日志相關(guān)
*/
private static final Logger LOGGER = LoggerFactory.getLogger(AESUtil.class);
/**
* 編碼
*/
private static final String ENCODING = "UTF-8";
/**
* 算法定義
*/
private static final String AES_ALGORITHM = "AES";
/**
* 指定填充方式
*/
private static final String CIPHER_PADDING = "AES/ECB/PKCS5Padding";
private static final String CIPHER_CBC_PADDING = "AES/CBC/PKCS5Padding";
/**
* 偏移量(CBC中使用,增強加密算法強度)
*/
private static final String IV_SEED = "1234567812345678";
/**
* AES加密
* @param content 待加密內(nèi)容
* @param aesKey 密碼
* @return
*/
public static String encrypt(String content, String aesKey){
if(StringUtils.isBlank(content)){
LOGGER.info("AES encrypt: the content is null!");
return null;
}
//判斷秘鑰是否為16位
if(StringUtils.isNotBlank(aesKey) && aesKey.length() == 16){
try {
//對密碼進(jìn)行編碼
byte[] bytes = aesKey.getBytes(ENCODING);
//設(shè)置加密算法,生成秘鑰
SecretKeySpec skeySpec = new SecretKeySpec(bytes, AES_ALGORITHM);
// "算法/模式/補碼方式"
Cipher cipher = Cipher.getInstance(CIPHER_PADDING);
//選擇加密
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
//根據(jù)待加密內(nèi)容生成字節(jié)數(shù)組
byte[] encrypted = cipher.doFinal(content.getBytes(ENCODING));
//返回base64字符串
return Base64Utils.encodeToString(encrypted);
} catch (Exception e) {
LOGGER.info("AES encrypt exception:" + e.getMessage());
throw new RuntimeException(e);
}
}else {
LOGGER.info("AES encrypt: the aesKey is null or error!");
return null;
}
}
/**
* 解密
*
* @param content 待解密內(nèi)容
* @param aesKey 密碼
* @return
*/
public static String decrypt(String content, String aesKey){
if(StringUtils.isBlank(content)){
LOGGER.info("AES decrypt: the content is null!");
return null;
}
//判斷秘鑰是否為16位
if(StringUtils.isNotBlank(aesKey) && aesKey.length() == 16){
try {
//對密碼進(jìn)行編碼
byte[] bytes = aesKey.getBytes(ENCODING);
//設(shè)置解密算法,生成秘鑰
SecretKeySpec skeySpec = new SecretKeySpec(bytes, AES_ALGORITHM);
// "算法/模式/補碼方式"
Cipher cipher = Cipher.getInstance(CIPHER_PADDING);
//選擇解密
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
//先進(jìn)行Base64解碼
byte[] decodeBase64 = Base64Utils.decodeFromString(content);
//根據(jù)待解密內(nèi)容進(jìn)行解密
byte[] decrypted = cipher.doFinal(decodeBase64);
//將字節(jié)數(shù)組轉(zhuǎn)成字符串
return new String(decrypted, ENCODING);
} catch (Exception e) {
LOGGER.info("AES decrypt exception:" + e.getMessage());
throw new RuntimeException(e);
}
}else {
LOGGER.info("AES decrypt: the aesKey is null or error!");
return null;
}
}
/**
* AES_CBC加密
*
* @param content 待加密內(nèi)容
* @param aesKey 密碼
* @return
*/
public static String encryptCBC(String content, String aesKey){
if(StringUtils.isBlank(content)){
LOGGER.info("AES_CBC encrypt: the content is null!");
return null;
}
//判斷秘鑰是否為16位
if(StringUtils.isNotBlank(aesKey) && aesKey.length() == 16){
try {
//對密碼進(jìn)行編碼
byte[] bytes = aesKey.getBytes(ENCODING);
//設(shè)置加密算法,生成秘鑰
SecretKeySpec skeySpec = new SecretKeySpec(bytes, AES_ALGORITHM);
// "算法/模式/補碼方式"
Cipher cipher = Cipher.getInstance(CIPHER_CBC_PADDING);
//偏移
IvParameterSpec iv = new IvParameterSpec(IV_SEED.getBytes(ENCODING));
//選擇加密
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
//根據(jù)待加密內(nèi)容生成字節(jié)數(shù)組
byte[] encrypted = cipher.doFinal(content.getBytes(ENCODING));
//返回base64字符串
return Base64Utils.encodeToString(encrypted);
} catch (Exception e) {
LOGGER.info("AES_CBC encrypt exception:" + e.getMessage());
throw new RuntimeException(e);
}
}else {
LOGGER.info("AES_CBC encrypt: the aesKey is null or error!");
return null;
}
}
/**
* AES_CBC解密
*
* @param content 待解密內(nèi)容
* @param aesKey 密碼
* @return
*/
public static String decryptCBC(String content, String aesKey){
if(StringUtils.isBlank(content)){
LOGGER.info("AES_CBC decrypt: the content is null!");
return null;
}
//判斷秘鑰是否為16位
if(StringUtils.isNotBlank(aesKey) && aesKey.length() == 16){
try {
//對密碼進(jìn)行編碼
byte[] bytes = aesKey.getBytes(ENCODING);
//設(shè)置解密算法,生成秘鑰
SecretKeySpec skeySpec = new SecretKeySpec(bytes, AES_ALGORITHM);
//偏移
IvParameterSpec iv = new IvParameterSpec(IV_SEED.getBytes(ENCODING));
// "算法/模式/補碼方式"
Cipher cipher = Cipher.getInstance(CIPHER_CBC_PADDING);
//選擇解密
cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
//先進(jìn)行Base64解碼
byte[] decodeBase64 = Base64Utils.decodeFromString(content);
//根據(jù)待解密內(nèi)容進(jìn)行解密
byte[] decrypted = cipher.doFinal(decodeBase64);
//將字節(jié)數(shù)組轉(zhuǎn)成字符串
return new String(decrypted, ENCODING);
} catch (Exception e) {
LOGGER.info("AES_CBC decrypt exception:" + e.getMessage());
throw new RuntimeException(e);
}
}else {
LOGGER.info("AES_CBC decrypt: the aesKey is null or error!");
return null;
}
}
public static void main(String[] args) {
// AES支持三種長度的密鑰:128位、192位、256位。
// 代碼中這種就是128位的加密密鑰,16字節(jié) * 8位/字節(jié) = 128位。
String random = RandomStringUtils.random(16, "abcdefghijklmnopqrstuvwxyz1234567890");
System.out.println("隨機key:" + random);
System.out.println();
System.out.println("---------加密---------");
String aesResult = encrypt("測試AES加密12", random);
System.out.println("aes加密結(jié)果:" + aesResult);
System.out.println();
System.out.println("---------解密---------");
String decrypt = decrypt(aesResult, random);
System.out.println("aes解密結(jié)果:" + decrypt);
System.out.println();
System.out.println("--------AES_CBC加密解密---------");
String cbcResult = encryptCBC("測試AES加密12456", random);
System.out.println("aes_cbc加密結(jié)果:" + cbcResult);
System.out.println();
System.out.println("---------解密CBC---------");
String cbcDecrypt = decryptCBC(cbcResult, random);
System.out.println("aes解密結(jié)果:" + cbcDecrypt);
System.out.println();
}
}
3.字段處理類
import com.demo.util.AESUtil;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* <p> @Title MyEncryptTypeHandler
* <p> @Description 字段加密處理
*
* @author ACGkaka
* @date 2023/2/21 17:20
*/
public class MyEncryptTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, AESUtil.defaultEncrypt(parameter));
}
@Override
public String getNullableResult(ResultSet rs, String column) throws SQLException {
return AESUtil.defaultDecrypt(rs.getString(column));
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return AESUtil.defaultDecrypt(rs.getString(columnIndex));
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return AESUtil.defaultDecrypt(cs.getString(columnIndex));
}
}
4.修改 MyBatis Plus 查詢
4.1 修改表對應(yīng)實體類
設(shè)置 @TableName
注解的 autoResultMap
為 true,默認(rèn) false。
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
/**
* 用戶表
*
* @author ACGkaka
* @date 2023/2/21 17:20
*/
@Data
@TableName(value = "t_user_info", autoResultMap = true)
public class UserInfo implements Serializable {}
4.2 修改加密字段對應(yīng)屬性
設(shè)置 @TableField
注解的 typeHandler
為 MyEncryptTypeHandler.class
。
import com.demo.encrypt.MyEncryptTypeHandler;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
/**
* 用戶表
*
* @author ACGkaka
* @date 2023/2/21 17:20
*/
@Data
@TableName(value = "t_user_info", autoResultMap = true)
public class UserInfo implements Serializable {
/**
* 手機號碼
*/
@TableField(value = "PHONE", typeHandler = MyEncryptTypeHandler.class)
private String phone;
}
4.3 修改 xml 使用 ResultMap
1)創(chuàng)建 ResultMap
映射,指定 typeHandler
;
2)查詢語句使用 ResultMap
返回。
<!-- 通用查詢映射結(jié)果 -->
<resultMap id="BaseResultMap" type="com.demo.model.UserInfo">
<id column="ID" property="id" />
<result column="ACCOUNT" property="staffCode" />
<result column="PHONE" property="phone" typeHandler="com.demo.encrypt.MyEncryptTypeHandler" />
</resultMap>
<!-- 查詢?nèi)?-->
<select id="findAll" resultMap="BaseResultMap">
SELECT * FROM t_user_info
</select>
4.4 修改 xml 中 el 表達(dá)式
設(shè)置好 4.1 和 4.2 就可以保證
修改前:
<!-- 更新手機號 -->
<update id="updatePhoneById">
update t_user_info set phone = #{phone} where id = #{id}
</update>
<!-- 根據(jù)手機號查詢 -->
<select id="findByPhone" resultMap="BaseResultMap">
SELECT * FROM t_user_info where phone = #{phone}
</select>
修改后:
<!-- 更新手機號 -->
<update id="updatePhoneById">
update t_user_info set phone = #{phone, typeHandler=com.demo.encrypt.MyEncryptTypeHandler} where id = #{id}
</update>
<!-- 根據(jù)手機號查詢 -->
<select id="findByPhone" resultMap="BaseResultMap">
SELECT * FROM t_user_info where phone = #{phone, typeHandler=com.demo.encrypt.MyEncryptTypeHandler}
</select>
5.測試結(jié)果
由于測試內(nèi)容較多,這里先直接展示測試結(jié)果,具體測試示例可以看 補充:測試實例
操作 | 實現(xiàn)方式 | 入?yún)?/th> | 測試結(jié)果 |
---|---|---|---|
SELECT | 原生SQL | 非加密字段 | 出參解密成功 |
SELECT | QueryWrapper | 非加密字段 | 出參解密成功 |
SELECT | 原生SQL | 加密字段 | 入?yún)⒓用艹晒?/font> |
SELECT | QueryWrapper | 加密字段 | 入?yún)⒓用苁?/font> |
UPDATE | 原生SQL | 加密字段 | 入?yún)⒓用艹晒?/font> |
UPDATE | UpdateWrapper | 加密字段 | 入?yún)⒓用苁?/font> |
UPDATE | LambdaUpdateWrapper | 加密字段 | 入?yún)⒓用艹晒?/font> |
UPDATE | updateById | 加密字段 | 入?yún)⒓用艹晒?/font> |
INSERT | Service | 加密字段 | 入?yún)⒓用艹晒?/font> |
說明:
- 官方的解答是 QueryWrapper、UpdateWrapper 底層是通過 @Param 來實現(xiàn)的,目前沒有做到入?yún)⒅С?typeHandler,如果做的話會影響性能。
- LambdaUpdateWrapper 要求 MyBatis-Plus 版本為 3.5.3,PageHelper 也需要升級為 5.1.11,但是升級之后 PageHelper 分頁不好使了,待優(yōu)化。(升級后依賴參考 補充:2.3)
6.MyBatis Plus 缺陷
-
QueryWrapper 不支持入?yún)⒓用埽?/font>
-
UpdateWrapper 不支持入?yún)⒓用埽?/font>
-
加密字段不支持模糊查詢。
7.歷史數(shù)據(jù)加密處理程序
@Override
public void encryptUser() {
// 加密 用戶信息
int count = this.count();
int pageSize = 1000;
int pageCount = count / pageSize + 1;
// 必須用唯一且非空字段進(jìn)行排序,否則 pageHelper 查出來的數(shù)據(jù)可能會有重復(fù)。
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<UserInfo>().orderByAsc("id");
for (int i = 0; i < pageCount; i++) {
log.info(">>>>>>>>>> 【INFO】加密用戶信息,當(dāng)前頁數(shù):{},總頁數(shù):{}", i + 1, pageCount);
PageHelper.startPage(i + 1, pageSize);
List<UserInfo> users = this.list(queryWrapper);
new PageInfo<>(users);
users.parallelStream().forEach(o -> {
// 解密重復(fù)加密手機號
while (AESUtil.defaultDecrypt(o.getPhoneNumber()) != null) {
o.setPhoneNumber(AESUtil.defaultDecrypt(o.getPhoneNumber()));
}
// 解密重復(fù)加密身份證號
while (AESUtil.defaultDecrypt(o.getIdCard()) != null) {
o.setIdCard(AESUtil.defaultDecrypt(o.getIdCard()));
}
});
this.updateBatchById(users);
}
}
一般手機號AES加密后長度為32,我們可以根據(jù)這點通過SQL檢查加密情況:
select '未加密數(shù)量' state, COUNT(*) from t_user_info where length(phone_number) < 32
union all
select '重復(fù)加密數(shù)量' state, COUNT(*) from t_user_info where length(phone_number) > 32;
補充:測試實例
1 查詢測試
1.1 查詢信息,SQL實現(xiàn)
@Test
public void getUserInfoTest1() {
UserInfo userInfo = userInfoService.findByAccount("testAccount");
System.out.println("userInfo:" + userInfo);
System.out.println("phone:" + userInfo.getPhone());
}
測試結(jié)果:出參解密成功
1.2 查詢信息,QueryWrapper實現(xiàn)
@Test
public void getUserInfoTest2() {
QueryWrapper<UserInfo> wrapper = new QueryWrapper<>();
wrapper.eq("account", "testAccount");
List<UserInfo> users = userInfoService.list(wrapper);
System.out.println("userInfo:" + users);
System.out.println("phone:" + users.get(0).getPhone());
}
測試結(jié)果:出參解密成功
1.3 查詢信息,根據(jù)加密字段查詢,SQL實現(xiàn)
@Test
public void getUserInfoTest3() {
UserInfo user = userInfoService.findByPhone("13888888888");
System.out.println("userInfo:" + user);
System.out.println("phone:" + user.getPhone());
}
(注意:入?yún)⑿枰褂胑l表達(dá)式指定 typeHandler)
測試結(jié)果:入?yún)⒓用艹晒?/font>
1.4 查詢信息,根據(jù)加密字段查詢,QueryWrapper實現(xiàn)
@Test
public void getUserInfoTest3() {
QueryWrapper<UserInfo> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(UserInfo::getPhone, "13888888888");
List<UserInfo> users = userInfoService.list(wrapper);
System.out.println("userInfo:" + users);
System.out.println("phone:" + users.get(0).getPhone());
}
測試結(jié)果:入?yún)⒓用苁?,QueryWrapper底層使用 @Param 實現(xiàn),無法像 SQL 實現(xiàn)一樣指定 typeHandler。
2.測試更新
2.1 更新信息,SQL實現(xiàn)
@Test
public void updateUserInfoTest1() {
userInfoService.updatePhoneByAccount("testAccount", "13888888888");
}
測試結(jié)果:入?yún)⒓用艹晒?/font>
2.2 更新信息,UpdateWrapper實現(xiàn)
@Test
public void updateUserInfoTest2() {
UpdateWrapper<UserInfo> wrapper = new UpdateWrapper<>();
wrapper.set("phone", "13888888888");
wrapper.eq("account", "testAccount");
userInfoService.update(wrapper);
getUserInfoTest1();
}
測試結(jié)果:入?yún)⒓用苁?,UpdateWrapper底層使用 @Param 實現(xiàn),無法像 SQL 實現(xiàn)一樣指定 typeHandler。
2.3 更新信息,LambdaUpdateWrapper實現(xiàn)
@Test
public void updateUserInfoTest3() {
LambdaUpdateWrapper<UserInfo> wrapper = Wrappers.<UserInfo>lambdaUpdate()
.set(UserInfo::getPhone, "13888888888", "typeHandler=com.demo.encrypt.MyEncryptTypeHandler");
wrapper.eq(UserInfo::getAccount, "testAccount");
userInfoService.update(wrapper);
getUserInfoTest1();
}
測試結(jié)果:入?yún)⒓用艹晒Γ?.5.3支持,但是升級之后 PageHelper 分頁不好使了,待優(yōu)化)
升級后依賴:
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<!-- mybatis的分頁插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.11</version>
<!-- pagehelper 包含該依賴存在版本沖突,因此不建議和 mp 一起混用 -->
<exclusions>
<exclusion>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
</exclusion>
</exclusions>
</dependency>
2.4 更新信息,updateById實現(xiàn)
@Test
public void updateUserInfoTest4() {
UserInfo userInfo = userInfoService.findByAccount("testAccount");
userInfo.setPhone("13888888888");
userInfoService.updateById(userInfo);
}
測試結(jié)果:入?yún)⒓用艹晒?/font>
3.測試插入
7.3.1 插入信息,SQL實現(xiàn)
@Test
public void insertUserInfoTest1() {
UserInfo userInfo = userInfoService.findByAccount("testAccount");
userInfo.setAccount("testAccount_002");
userInfo.setPhone("13888888888");
userInfoService.save(userInfo);
UserInfo newUserInfo = userInfoService.findByAccount("testAccount_002");
System.out.println("userInfo:" + newUserInfo);
System.out.println("phone:" + newUserInfo.getPhone());
}
測試結(jié)果:入?yún)⒓用艹晒?/font>
3.2 插入信息,Service實現(xiàn)
@Test
public void insertUserInfoTest1() {
UserInfo userInfo = userInfoService.findByAccount("testAccount");
userInfo.setAccount("testAccount_002");
userInfo.setPhone("13888888888");
userInfoService.save(userInfo);
UserInfo newUserInfo = userInfoService.findByAccount("testAccount_002");
System.out.println("userInfo:" + newUserInfo);
System.out.println("phone:" + newUserInfo.getPhone());
}
測試結(jié)果:入?yún)⒓用艹晒?/font>
整理完畢,完結(jié)撒花~
參考地址:
1.mybaits plus 字段加密與解密,https://blog.csdn.net/qq_21134059/article/details/121752978
2.mybatis plus 官方問題頁面,https://github.com/baomidou/mybatis-plus/issues
3.更新時自定義的TypeHandler不生效,https://github.com/baomidou/mybatis-plus/issues/794
4.lambdaUpdate() 無法更新Json對象字段,https://github.com/baomidou/mybatis-plus/issues/5031文章來源:http://www.zghlxwxcb.cn/news/detail-465831.html
5.LambdaUpdateWrapper不支持自定義BaseTypeHandler,https://github.com/baomidou/mybatis-plus/issues/3317文章來源地址http://www.zghlxwxcb.cn/news/detail-465831.html
到了這里,關(guān)于MyBatis Plus 數(shù)據(jù)庫字段加密處理的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!