Mybatis敏感數(shù)據(jù)加解密插件實(shí)現(xiàn)過(guò)程
本文將詳細(xì)介紹基于MyBatis的敏感數(shù)據(jù)全鏈路安全處理方案,涵蓋插件開發(fā)原理、具體實(shí)現(xiàn)代碼以及數(shù)據(jù)庫(kù)函數(shù)集成。
1. MyBatis插件開發(fā)原理
1.1 插件的作用與攔截機(jī)制
MyBatis 插件是基于攔截器模式實(shí)現(xiàn)的,允許開發(fā)者在 MyBatis 執(zhí)行 SQL語(yǔ)句的關(guān)鍵節(jié)點(diǎn)插入自定義邏輯。MyBatis 支持對(duì)四大核心組件進(jìn)行攔截:
- Executor:執(zhí)行器,用于攔截增刪改查操作
- ParameterHandler:參數(shù)處理器,用于攔截參數(shù)設(shè)置
- ResultSetHandler:結(jié)果集處理器,用于攔截結(jié)果集處理
- StatementHandler:語(yǔ)句處理器,用于攔截SQL語(yǔ)句構(gòu)建
插件通過(guò)實(shí)現(xiàn) Interceptor 接口并配合@Intercepts和@Signature注解來(lái)定義攔截規(guī)則。
1.2 插件的運(yùn)行機(jī)制與責(zé)任鏈模式
MyBatis的插件體系采用責(zé)任鏈模式,通過(guò)InterceptorChain管理所有攔截器。當(dāng)執(zhí)行目標(biāo)方法時(shí),會(huì)依次調(diào)用所有注冊(cè)的攔截器。具體實(shí)現(xiàn)機(jī)制如下:
// 插件動(dòng)態(tài)代理示例
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
}
return target;
}
動(dòng)態(tài)代理機(jī)制確保在目標(biāo)方法執(zhí)行前后可以插入自定義邏輯,這是實(shí)現(xiàn)數(shù)據(jù)加解密的理論基礎(chǔ)。
2. MyBatis + 注解實(shí)現(xiàn)加解密插件
2.1 整體架構(gòu)設(shè)計(jì)
- 本方案采用分層處理策略,將加解密與脫敏分離:
- 數(shù)據(jù)訪問(wèn)層:通過(guò)MyBatis插件實(shí)現(xiàn)自動(dòng)加解密
- API展示層:通過(guò)Jackson序列化器實(shí)現(xiàn)數(shù)據(jù)脫敏
2.2 注解定義
首先定義用于標(biāo)記敏感數(shù)據(jù)的注解:
// 類級(jí)注解,標(biāo)記需要加解密的實(shí)體
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {
}
// 加解密字段注解
@Inherited
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveField {
}
2.3 加密解密方法或函數(shù)工具類
以 pgsql 中的加解密函數(shù)作為舉例,實(shí)現(xiàn)加密解密方法調(diào)用:
@Component
public class PgCryptoUtil {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 加密方法 - 返回字符串格式(如十六進(jìn)制)
*/
public String encrypt(String plainText, String key) throws Exception {
String sql = "SELECT encode(pgp_sym_encrypt(?, ?), 'hex')";
return jdbcTemplate.queryForObject(sql, String.class, plainText, key);
}
/**
* 解密方法 - 接收字符串格式的加密數(shù)據(jù)
*/
public String decrypt(String encryptedText, String key) throws Exception {
// 假設(shè)encryptedText是十六進(jìn)制格式的字符串
String sql = "SELECT pgp_sym_decrypt(decode(?, 'hex'), ?)";
return jdbcTemplate.queryForObject(sql, String.class, encryptedText, key);
}
}
2.4 基于攔截器的加解密實(shí)現(xiàn)
2.4.1 參數(shù)加密攔截器
攔截ParameterHandler.setParameters方法,在數(shù)據(jù)入庫(kù)前進(jìn)行加密。
// 加密攔截器 - 用于處理INSERT/UPDATE操作
@Component
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class EncryptInterceptor implements Interceptor {
@Autowired
private PgCryptoUtil pgCryptoUtil;
private final String ENCRYPTION_KEY = "1234567890abcdef";
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
Object parameter = args[1];
// 加密處理
if (parameter != null) {
encryptParameters(parameter);
}
return invocation.proceed();
}
private void encryptParameters(Object parameter) {
if (parameter instanceof List) {
for (Object item : (List) parameter) {
encryptItem(item);
}
} else {
encryptItem(parameter);
}
}
private void encryptItem(Object item) {
if (item != null && item.getClass().isAnnotationPresent(SensitiveData.class)) {
MetaObject metaObject = SystemMetaObject.forObject(item);
Field[] fields = item.getClass().getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(SensitiveField.class) && field.getType() == String.class) {
String fieldName = field.getName();
Object value = metaObject.getValue(fieldName);
if (value instanceof String && !((String) value).isEmpty()) {
try {
// 加密字符串?dāng)?shù)據(jù)
String encryptedValue = pgCryptoUtil.encrypt((String) value, ENCRYPTION_KEY);
metaObject.setValue(fieldName, encryptedValue);
} catch (Exception e) {
throw new RuntimeException("加密字段 " + fieldName + " 時(shí)發(fā)生錯(cuò)誤", e);
}
}
}
}
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
2.4.2 結(jié)果集解密攔截器
攔截ResultSetHandler.handleResultSets方法,在數(shù)據(jù)查詢后進(jìn)行解密
// 解密攔截器 - 用于處理SELECT操作(您的代碼優(yōu)化版)
@Component
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class DecryptInterceptor implements Interceptor {
@Autowired
private PgCryptoUtil pgCryptoUtil;
private final String ENCRYPTION_KEY = "1234567890abcdef";
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object result = invocation.proceed();
if (result == null) {
return null;
}
if (result instanceof List) {
for (Object item : (List) result) {
decryptItem(item);
}
} else {
decryptItem(result);
}
return result;
}
private void decryptItem(Object item) {
if (item != null && item.getClass().isAnnotationPresent(SensitiveData.class)) {
MetaObject metaObject = SystemMetaObject.forObject(item);
Field[] fields = item.getClass().getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(SensitiveField.class) && field.getType() == String.class) {
String fieldName = field.getName();
Object value = metaObject.getValue(fieldName);
// 關(guān)鍵修改:處理String類型的加密數(shù)據(jù)
if (value instanceof String && !((String) value).isEmpty()) {
try {
String decryptedValue = pgCryptoUtil.decrypt((String) value, ENCRYPTION_KEY);
metaObject.setValue(fieldName, decryptedValue);
} catch (Exception e) {
// 記錄解密失敗但不要拋出異常,以免影響正常查詢
System.err.println("解密字段 " + fieldName + " 失敗: " + e.getMessage());
// 可以選擇保留原始加密值或設(shè)置為null
// metaObject.setValue(fieldName, null);
}
}
}
}
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
2.5 實(shí)體類使用
@SensitiveData
@Data // lombok注解
public class UserInfo extends BaseEntity implements Serializable {
/**
* 主鍵
*/
private String id;
/**
*手機(jī)號(hào)
*/
@SensitiveField
@Excel(name = "手機(jī)號(hào)")
private String phone;
}
3. 數(shù)據(jù)庫(kù)層面加解密函數(shù)
3.1 MySQL加解密函數(shù)
MySQL原生支持AES加解密函數(shù),可在SQL層面實(shí)現(xiàn)加解密:
-- 加密數(shù)據(jù)插入
INSERT INTO users (username, phone_encrypted)
VALUES ('john_doe', AES_ENCRYPT('13812345678', 'encryption_key'));
-- 查詢解密數(shù)據(jù)
SELECT username, AES_DECRYPT(phone_encrypted, 'encryption_key') as phone
FROM users WHERE username = 'john_doe';
3.2 PostgreSQL加解密函數(shù)
PostgreSQL通過(guò)pgcrypto擴(kuò)展提供加密功能:
-- 啟用pgcrypto擴(kuò)展
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- 加密數(shù)據(jù)插入
INSERT INTO users (username, phone_encrypted)
VALUES ('john_doe', pgp_sym_encrypt('13812345678', 'encryption_key'));
-- 查詢解密數(shù)據(jù)
SELECT username, pgp_sym_decrypt(phone_encrypted, 'encryption_key') as phone
FROM users WHERE username = 'john_doe';
-- 使用自定義函數(shù)簡(jiǎn)化操作
CREATE FUNCTION encrypt_value(value TEXT, key TEXT)
RETURNS TEXT AS $$
BEGIN
RETURN encode(pgp_sym_encrypt(value, key), 'base64');
END;
$$ LANGUAGE plpgsql;
單純?cè)?PostgreSQL 中使用更安全的加密方式,推薦使用其默認(rèn)的 PGP 函數(shù)。注意:此方法與 MySQL 默認(rèn)加密不兼容。
-- 加密 (使用默認(rèn)更安全的CBC模式等選項(xiàng))
SELECT armor(pgp_sym_encrypt('hello', 'mykey')) AS encrypted_data_pgp;
-- 解密
SELECT pgp_sym_decrypt(dearmor('<上述armor函數(shù)輸出的結(jié)果>'), 'mykey') AS decrypted_data_pgp;
3.3 支持 mysql和 pgsql 的通用函數(shù)定義
由于兩種數(shù)據(jù)庫(kù)默認(rèn)的加密模式不同,為了實(shí)現(xiàn)兼容,核心思路是在 PostgreSQL 端使用 encrypt和 decrypt函數(shù),并明確指定加密參數(shù),以模擬 MySQL 的行為。
以下示例均使用明文 hello和密鑰 mykey。
1. MySQL 加解密示例
MySQL 使用默認(rèn)的 AES-128-ECB 模式。
-- 加密 (結(jié)果為二進(jìn)制, 通常用 HEX 轉(zhuǎn)換為十六進(jìn)制字符串存儲(chǔ)或查看)
SELECT HEX(AES_ENCRYPT('hello', 'mykey')) AS encrypted_data;
-- 結(jié)果示例: '744661B08EB4698B64DE167B7B43BCD7'
-- 解密 (需先將十六進(jìn)制字符串轉(zhuǎn)換回二進(jìn)制)
SELECT AES_DECRYPT(UNHEX('744661B08EB4698B64DE167B7B43BCD7'), 'mykey') AS decrypted_data;
-- 結(jié)果: 'hello'
2. PostgreSQL 兼容 MySQL 的加解密示例
在 PostgreSQL 中,使用 pgcrypto擴(kuò)展的 encrypt/decrypt函數(shù),并指定算法和模式為 aes-ecb以達(dá)到兼容。
-- 首先確保啟用 pgcrypto 擴(kuò)展
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- 加密 (使用 'aes-ecb' 模式模擬MySQL,結(jié)果轉(zhuǎn)換為十六進(jìn)制以便比較和存儲(chǔ))
SELECT encode(encrypt('hello', 'mykey', 'aes-ecb'), 'hex') AS encrypted_data;
-- 結(jié)果應(yīng)與MySQL一致: '744661B08EB4698B64DE167B7B43BCD7'
-- 解密
SELECT convert_from(decrypt(decode('744661B08EB4698B64DE167B7B43BCD7', 'hex'), 'mykey', 'aes-ecb'), 'utf8') AS decrypted_data;
-- 結(jié)果: 'hello'
四、相關(guān)開源項(xiàng)目 mybatis-encrypt-plugin 介紹
mybatis-encrypt-plugin 是一款開源的基于mybatis的插件機(jī)制編寫的一套敏感數(shù)據(jù)加解密以及數(shù)據(jù)脫敏工具。
開源地址:https://github.com/andydazhong/mybatis-encrypt-plugin
1. 實(shí)現(xiàn)原理
- 攔截mybatis的StatementHandler 對(duì)讀寫請(qǐng)求進(jìn)行脫敏和字段的加密。
- 攔截mybatis的ResultSetHandler,對(duì)讀請(qǐng)求的響應(yīng)進(jìn)行加密字段的解密賦值。
2. 使用方式
1. 導(dǎo)入依賴
<!-- mybatis數(shù)據(jù)脫敏插件 -->
<dependency>
<groupId>com.github.chenhaiyangs</groupId>
<artifactId>mybatis-encrypt-plugin</artifactId>
<version>1.0.0</version>
</dependency>
2. 編寫加解密實(shí)現(xiàn)類以及配置mybatis的插件
下面在springboot場(chǎng)景下的一個(gè)配置案例。
/**
* 插件配置
*/
@Configuration
public class EncryptPluginConfig {
//加密方式
@Bean
Encrypt encryptor() throws Exception{
return new AesSupport("1870577f29b17d6787782f35998c4a79");
}
//配置插件
@Bean
ConfigurationCustomizer configurationCustomizer() throws Exception{
DecryptReadInterceptor decryptReadInterceptor = new DecryptReadInterceptor(encryptor());
SensitiveAndEncryptWriteInterceptor sensitiveAndEncryptWriteInterceptor = new SensitiveAndEncryptWriteInterceptor(encryptor());
return (configuration) -> {
configuration.addInterceptor(decryptReadInterceptor);
configuration.addInterceptor(sensitiveAndEncryptWriteInterceptor);
};
}
}
3. 在vo類上添加功能注解使得插件生效
@SensitiveEncryptEnabled
@Data
public class UserDTO {
private Integer id;
/**
* 用戶名
*/
@EncryptField
private String userName;
/**
* 脫敏的用戶名
*/
@SensitiveField(SensitiveType.CHINESE_NAME)
private String userNameSensitive;
/**
* 值的賦值不從數(shù)據(jù)庫(kù)取,而是從userName字段獲得。
*/
@SensitiveBinded(bindField = "userName",value = SensitiveType.CHINESE_NAME)
private String userNameOnlyDTO;
/**
* 身份證號(hào)
*/
@EncryptField
private String idcard;
/**
* 脫敏的身份證號(hào)
*/
@SensitiveField(SensitiveType.ID_CARD)
private String idcardSensitive;
/**
* 一個(gè)json串,需要脫敏
* SensitiveJSONField標(biāo)記json中需要脫敏的字段
*/
@SensitiveJSONField(sensitivelist = {
@SensitiveJSONFieldKey(key = "idcard",type = SensitiveType.ID_CARD),
@SensitiveJSONFieldKey(key = "username",type = SensitiveType.CHINESE_NAME),
})
private String jsonStr;
private int age;
@SensitiveField(SensitiveType.EMAIL)
private String email;
}
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
MyBatis的?級(jí)映射及延遲加載過(guò)程詳解
這篇文章主要介紹了MyBatis的?級(jí)映射及延遲加載,包括多對(duì)一延時(shí)加載方式及一對(duì)多,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-02-02
SpringSecurity自定義登錄接口的實(shí)現(xiàn)
本文介紹了使用Spring Security實(shí)現(xiàn)自定義登錄接口,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-01-01
MyBatisPlus?大數(shù)據(jù)量查詢慢的問(wèn)題解決
本文主要介紹了MyBatis?Plus?解決大數(shù)據(jù)量查詢慢問(wèn)題,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02
Java/Android 獲取網(wǎng)絡(luò)重定向文件的真實(shí)URL的示例代碼
本篇文章主要介紹了Java/Android 獲取網(wǎng)絡(luò)重定向文件的真實(shí)URL的示例代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-11-11
springmvc實(shí)現(xiàn)跨服務(wù)器文件上傳功能
這篇文章主要為大家詳細(xì)介紹了springmvc實(shí)現(xiàn)跨服務(wù)器文件上傳功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-08-08

