SpringBoot實(shí)現(xiàn)加密字段模糊查詢的最佳實(shí)踐
前言
在數(shù)據(jù)安全日益重要的今天,數(shù)據(jù)庫加密已經(jīng)成為企業(yè)級(jí)應(yīng)用的標(biāo)配。然而,加密字段的模糊查詢一直是一個(gè)技術(shù)難題。傳統(tǒng)的加密方式(如AES)會(huì)將明文轉(zhuǎn)換為完全隨機(jī)的密文,導(dǎo)致無法直接使用LIKE語句進(jìn)行模糊匹配。本文將深入探討Spring Boot環(huán)境下實(shí)現(xiàn)加密字段模糊查詢的主流方案,結(jié)合理論分析和可直接用于生產(chǎn)的代碼示例,幫助開發(fā)者在保證數(shù)據(jù)安全的同時(shí),兼顧業(yè)務(wù)查詢需求
問題分析
1.1 傳統(tǒng)加密的局限性
傳統(tǒng)對(duì)稱加密(如AES)和非對(duì)稱加密(如RSA)都存在以下問題:
- 加密后的密文與明文沒有語義關(guān)聯(lián)
- 無法直接對(duì)密文進(jìn)行模糊查詢
- 暴力破解風(fēng)險(xiǎn)高(尤其是短密碼)
1.2 常見解決方案對(duì)比
| 方案 | 優(yōu)點(diǎn) | 缺點(diǎn) | 適用場(chǎng)景 |
|---|---|---|---|
| 明文存儲(chǔ) | 查詢性能好 | 數(shù)據(jù)安全風(fēng)險(xiǎn)高 | 非敏感數(shù)據(jù) |
| 哈希加密 | 安全性高 | 無法反向解密 | 密碼存儲(chǔ) |
| 同態(tài)加密 | 支持密文運(yùn)算 | 性能開銷大 | 高安全性要求場(chǎng)景 |
| 格式保留 | 加密保持?jǐn)?shù)據(jù)格式 | 實(shí)現(xiàn)復(fù)雜 | 需要保持?jǐn)?shù)據(jù)格式的場(chǎng)景 |
| 部分加密 | 平衡安全與性能 | 部分?jǐn)?shù)據(jù)暴露 | 非核心敏感數(shù)據(jù) |
主流技術(shù)方案
1 格式保留加密(Format-Preserving Encryption)
1.1 理論基礎(chǔ)
加密特點(diǎn):
- 密文與明文具有相同的格式(如都是數(shù)字字符串)
- 密文長(zhǎng)度與明文長(zhǎng)度完全一致
- 提供完整性保護(hù)
- 支持關(guān)聯(lián)數(shù)據(jù)(AAD)
實(shí)現(xiàn)原理:
使用確定性加密算法對(duì)明文進(jìn)行加密,然后使用哈希函數(shù)和模運(yùn)算
將加密結(jié)果映射到與明文相同格式的字符集上,同時(shí)保存足夠的信息以便解密。
1.2 依賴配置
增加pom配置依賴
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>com.google.crypto.tink</groupId>
<artifactId>tink</artifactId>
<version>1.10.0</version>
</dependency>
</dependencies>
1.3 完整實(shí)現(xiàn)代碼
FPE加密配置實(shí)現(xiàn)TrueFormatPreservingEncryption:
import com.google.crypto.tink.DeterministicAead;
import com.google.crypto.tink.KeysetHandle;
import com.google.crypto.tink.daead.DeterministicAeadConfig;
import com.google.crypto.tink.daead.DeterministicAeadFactory;
import com.google.crypto.tink.daead.DeterministicAeadKeyTemplates;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
/**
* 真正的格式保留加密實(shí)現(xiàn)
* @author senfel
* @date 2026/2/12 15:06
*/
public class TrueFormatPreservingEncryption {
private final DeterministicAead daead;
private final MessageDigest hashFunction;
/**
* 構(gòu)造函數(shù)
* @throws GeneralSecurityException 如果密鑰生成或初始化失敗
*/
public TrueFormatPreservingEncryption() throws GeneralSecurityException {
// 注冊(cè)確定性 AEAD 配置
DeterministicAeadConfig.register();
// 創(chuàng)建 AES-SIV 密鑰模板
KeysetHandle keysetHandle = KeysetHandle.generateNew(
DeterministicAeadKeyTemplates.AES256_SIV
);
// 獲取確定性 AEAD 原語
this.daead = DeterministicAeadFactory.getPrimitive(keysetHandle);
// 初始化哈希函數(shù)用于格式轉(zhuǎn)換
try {
this.hashFunction = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new GeneralSecurityException("無法初始化哈希函數(shù)", e);
}
}
/**
* 加密明文
* @param plaintext 要加密的明文(如手機(jī)號(hào)、身份證號(hào)等)
* @return 加密后的密文,與明文具有相同的格式和長(zhǎng)度
* @throws GeneralSecurityException 如果加密失敗
*/
public String encrypt(String plaintext) throws GeneralSecurityException {
if (plaintext == null || plaintext.isEmpty()) {
return plaintext;
}
// 將明文轉(zhuǎn)換為字節(jié)數(shù)組
byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8);
// 使用確定性加密算法加密
byte[] encryptedBytes = daead.encryptDeterministically(plaintextBytes, null);
// 將加密結(jié)果轉(zhuǎn)換為與原始格式相同的字符串
// 使用加密后的字節(jié)數(shù)組作為種子,通過哈希和模運(yùn)算生成格式保留的密文
return convertToSameFormat(encryptedBytes, plaintext);
}
/**
* 解密密文
* @param ciphertext 要解密的密文(格式保留的字符串)
* @param originalPlaintext 原始明文(用于確定格式)
* @return 解密后的明文
* @throws GeneralSecurityException 如果解密失敗
*/
public String decrypt(String ciphertext, String originalPlaintext) throws GeneralSecurityException {
if (ciphertext == null || ciphertext.isEmpty()) {
return ciphertext;
}
// 由于格式保留加密是不可逆的(信息丟失),我們需要使用暴力搜索或字典
// 但這不是真正的格式保留加密的正確實(shí)現(xiàn)方式
// 更好的方法:我們需要存儲(chǔ)加密后的完整字節(jié)數(shù)組
// 但為了保持格式,我們可以使用一個(gè)映射表
// 實(shí)際上,真正的格式保留加密需要使用 FF1 或 FF3 算法
// 這里我們提供一個(gè)改進(jìn)的實(shí)現(xiàn):使用加密后的字節(jié)數(shù)組的哈希值來生成格式保留的密文
// 同時(shí)我們需要能夠恢復(fù)原始加密字節(jié)數(shù)組
throw new GeneralSecurityException("格式保留加密的解密需要原始加密上下文,當(dāng)前實(shí)現(xiàn)不支持直接解密格式保留的密文");
}
/**
* 改進(jìn)的解密方法:需要存儲(chǔ)完整的加密信息
* 在實(shí)際應(yīng)用中,應(yīng)該將加密后的完整字節(jié)數(shù)組(Base64編碼)與格式保留的密文一起存儲(chǔ)
*
* @param encryptedBytesBase64 Base64編碼的加密字節(jié)數(shù)組
* @return 解密后的明文
* @throws GeneralSecurityException 如果解密失敗
*/
public String decryptFromBase64(String encryptedBytesBase64) throws GeneralSecurityException {
if (encryptedBytesBase64 == null || encryptedBytesBase64.isEmpty()) {
return "";
}
try {
// 將Base64字符串解碼為字節(jié)數(shù)組
byte[] encryptedBytes = java.util.Base64.getDecoder().decode(encryptedBytesBase64);
// 解密
byte[] decrypted = daead.decryptDeterministically(encryptedBytes, null);
return new String(decrypted, StandardCharsets.UTF_8);
} catch (IllegalArgumentException e) {
throw new GeneralSecurityException("Base64解碼失敗", e);
}
}
/**
* 改進(jìn)的加密方法:返回格式保留的密文和Base64編碼的完整加密數(shù)據(jù)
*
* @param plaintext 要加密的明文
* @return 包含格式保留密文和完整加密數(shù)據(jù)的對(duì)象
* @throws GeneralSecurityException 如果加密失敗
*/
public EncryptionResult encryptWithFullData(String plaintext) throws GeneralSecurityException {
if (plaintext == null || plaintext.isEmpty()) {
return new EncryptionResult("", "");
}
// 將明文轉(zhuǎn)換為字節(jié)數(shù)組
byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8);
// 使用確定性加密算法加密
byte[] encryptedBytes = daead.encryptDeterministically(plaintextBytes, null);
// 將加密結(jié)果轉(zhuǎn)換為與原始格式相同的字符串
String formatPreservingCiphertext = convertToSameFormat(encryptedBytes, plaintext);
// 將完整的加密字節(jié)數(shù)組編碼為Base64
String encryptedBytesBase64 = java.util.Base64.getEncoder().encodeToString(encryptedBytes);
return new EncryptionResult(formatPreservingCiphertext, encryptedBytesBase64);
}
/**
* 將加密結(jié)果轉(zhuǎn)換為與原始格式相同的字符串
* 使用加密字節(jié)數(shù)組的哈希值作為隨機(jī)種子,確保相同輸入產(chǎn)生相同輸出(確定性)
*
* @param encryptedBytes 加密后的字節(jié)數(shù)組
* @param plaintext 原始明文(用于確定格式)
* @return 與原始格式相同的字符串
*/
private String convertToSameFormat(byte[] encryptedBytes, String plaintext) {
StringBuilder sb = new StringBuilder();
// 使用加密字節(jié)數(shù)組生成確定性哈希值
hashFunction.reset();
hashFunction.update(encryptedBytes);
byte[] hash = hashFunction.digest();
// 為了處理長(zhǎng)字符串,我們需要擴(kuò)展哈希值
List<Byte> hashBytes = new ArrayList<>();
for (byte b : hash) {
hashBytes.add(b);
}
// 如果哈希值不夠長(zhǎng),循環(huán)使用
int hashIndex = 0;
for (int i = 0; i < plaintext.length(); i++) {
char originalChar = plaintext.charAt(i);
// 獲取用于生成該字符的哈希字節(jié)
byte hashByte = hashBytes.get(hashIndex % hashBytes.size());
hashIndex++;
// 為了增加隨機(jī)性,也使用位置索引
hashFunction.reset();
hashFunction.update(encryptedBytes);
hashFunction.update(String.valueOf(i).getBytes(StandardCharsets.UTF_8));
byte[] positionHash = hashFunction.digest();
int combinedValue = (hashByte & 0xFF) ^ (positionHash[0] & 0xFF);
if (Character.isDigit(originalChar)) {
// 數(shù)字格式:將哈希值映射到 0-9
sb.append((char) ('0' + (combinedValue % 10)));
} else if (Character.isLetter(originalChar)) {
// 字母格式:將哈希值映射到字母
int offset = Character.isUpperCase(originalChar) ? 'A' : 'a';
sb.append((char) (offset + (combinedValue % 26)));
} else {
// 其他格式:保持原始字符(但這不是安全的,可以考慮加密)
sb.append(originalChar);
}
}
return sb.toString();
}
/**
* 加密結(jié)果類
* 包含格式保留的密文和完整的加密數(shù)據(jù)(Base64編碼)
*/
public static class EncryptionResult {
private final String formatPreservingCiphertext; // 格式保留的密文
private final String encryptedBytesBase64; // Base64編碼的完整加密數(shù)據(jù)
public EncryptionResult(String formatPreservingCiphertext, String encryptedBytesBase64) {
this.formatPreservingCiphertext = formatPreservingCiphertext;
this.encryptedBytesBase64 = encryptedBytesBase64;
}
public String getFormatPreservingCiphertext() {
return formatPreservingCiphertext;
}
public String getEncryptedBytesBase64() {
return encryptedBytesBase64;
}
}
}
1.4 測(cè)試用例
寫一個(gè)簡(jiǎn)單的FPE測(cè)試用例:
/**
* TrueFormatPreservingEncryption測(cè)試
* @param args 命令行參數(shù)
* @author senfel
* @date 2026/2/12 15:06
* @return void
*/
public static void main(String[] args) {
try {
TrueFormatPreservingEncryption fpe = new TrueFormatPreservingEncryption();
String plaintext = "13800138000";
System.out.println("=== 測(cè)試格式保留加密 ===");
// 方法1:僅格式保留加密(無法解密,因?yàn)樾畔G失)
//明文: 13800138000
//格式保留密文: 39508471166
//明文長(zhǎng)度: 11
//密文長(zhǎng)度: 11
//明文格式是否保留: true
String formatPreservingCiphertext = fpe.encrypt(plaintext);
System.out.println("明文: " + plaintext);
System.out.println("格式保留密文: " + formatPreservingCiphertext);
System.out.println("明文長(zhǎng)度: " + plaintext.length());
System.out.println("密文長(zhǎng)度: " + formatPreservingCiphertext.length());
System.out.println("明文格式是否保留: " + isSameFormat(plaintext, formatPreservingCiphertext));
System.out.println();
// 方法2:格式保留加密 + 完整加密數(shù)據(jù)(可解密)
EncryptionResult result = fpe.encryptWithFullData(plaintext);
System.out.println("=== 使用完整數(shù)據(jù)加密 ===");
//明文: 13800138000
//格式保留密文: 39508471166
//完整加密數(shù)據(jù)(Base64): AXRF5PpcdHbhqKhwTZAEkYwBlHrkQJ2J9PFRg8ApXtw=
//格式是否保留: true
//解密結(jié)果: 13800138000
//加密解密是否成功: true
System.out.println("明文: " + plaintext);
System.out.println("格式保留密文: " + result.getFormatPreservingCiphertext());
System.out.println("完整加密數(shù)據(jù)(Base64): " + result.getEncryptedBytesBase64());
System.out.println("格式是否保留: " + isSameFormat(plaintext, result.getFormatPreservingCiphertext()));
// 解密
String decrypted = fpe.decryptFromBase64(result.getEncryptedBytesBase64());
System.out.println("解密結(jié)果: " + decrypted);
System.out.println("加密解密是否成功: " + plaintext.equals(decrypted));
// 測(cè)試多次加密同一明文,驗(yàn)證確定性
System.out.println();
System.out.println("=== 測(cè)試確定性加密 ===");
//第一次加密: 39508471166
//第二次加密: 39508471166
//兩次加密結(jié)果是否相同(確定性): true
String ciphertext1 = fpe.encrypt(plaintext);
String ciphertext2 = fpe.encrypt(plaintext);
System.out.println("第一次加密: " + ciphertext1);
System.out.println("第二次加密: " + ciphertext2);
System.out.println("兩次加密結(jié)果是否相同(確定性): " + ciphertext1.equals(ciphertext2));
} catch (GeneralSecurityException e) {
System.err.println("加密解密失敗: " + e.getMessage());
e.printStackTrace();
}
}
/**
* 檢查兩個(gè)字符串是否具有相同的格式
* @param s1 第一個(gè)字符串
* @param s2 第二個(gè)字符串
* @return 是否具有相同的格式
*/
private static boolean isSameFormat(String s1, String s2) {
if (s1.length() != s2.length()) {
return false;
}
for (int i = 0; i < s1.length(); i++) {
char c1 = s1.charAt(i);
char c2 = s2.charAt(i);
if (Character.isDigit(c1) != Character.isDigit(c2)) {
return false;
}
if (Character.isLetter(c1) != Character.isLetter(c2)) {
return false;
}
if (Character.isUpperCase(c1) != Character.isUpperCase(c2)) {
return false;
}
}
return true;
}
2 AES部分加密與明文索引
2.1 理論基礎(chǔ)
加密特點(diǎn):
- 對(duì)敏感數(shù)據(jù)(如身份證號(hào))進(jìn)行部分加密
- 加密核心部分(前6位+后4位),保留中間部分作為明文索引
- 支持通過索引部分進(jìn)行模糊查詢,然后解密驗(yàn)證完整匹配
使用場(chǎng)景:
身份證號(hào):加密前6位(地區(qū)碼)+ 后4位(校驗(yàn)位),保留中間8位(出生日期)作為索引
手機(jī)號(hào):可以加密前3位+后4位,保留中間部分作為索引
2.2 完整實(shí)現(xiàn)代碼
增加一個(gè)用戶類:
import lombok.Getter;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* AES部分加密與明文索引
* @author senfel
* @date 2026/2/12
*/
public class AESPartialEncryption {
private static final String ALGORITHM = "AES";
private static final String TRANSFORMATION = "AES";
private final SecretKeySpec secretKey;
/**
* 構(gòu)造函數(shù)
*
* @param key AES密鑰(必須是16、24或32字節(jié))
* @throws IllegalArgumentException 如果密鑰長(zhǎng)度不正確
*/
public AESPartialEncryption(String key) {
if (key == null || key.isEmpty()) {
throw new IllegalArgumentException("密鑰不能為空");
}
byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
int keyLength = keyBytes.length;
// AES密鑰長(zhǎng)度必須是16、24或32字節(jié)
if (keyLength != 16 && keyLength != 24 && keyLength != 32) {
throw new IllegalArgumentException(
String.format("AES密鑰長(zhǎng)度必須是16、24或32字節(jié),當(dāng)前長(zhǎng)度:%d", keyLength)
);
}
this.secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
}
/**
* 使用默認(rèn)密鑰創(chuàng)建實(shí)例
*
* @return AESPartialEncryption實(shí)例
*/
public static AESPartialEncryption createDefault() {
return new AESPartialEncryption("1234567890123456");
}
/**
* 加密字符串
*
* @param plaintext 要加密的明文
* @return Base64編碼的密文
* @throws Exception 如果加密失敗
*/
public String encrypt(String plaintext) throws Exception {
if (plaintext == null || plaintext.isEmpty()) {
return plaintext;
}
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encryptedBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encryptedBytes);
}
/**
* 解密字符串
*
* @param ciphertext Base64編碼的密文
* @return 解密后的明文
* @throws Exception 如果解密失敗
*/
public String decrypt(String ciphertext) throws Exception {
if (ciphertext == null || ciphertext.isEmpty()) {
return ciphertext;
}
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(ciphertext));
return new String(decryptedBytes, StandardCharsets.UTF_8);
}
/**
* 部分加密身份證號(hào)
* 加密前6位(地區(qū)碼)+ 后4位(校驗(yàn)位),保留中間8位(出生日期)作為索引
*
* @param idCard 身份證號(hào)(18位)
* @return 加密結(jié)果對(duì)象,包含加密的核心部分和明文索引部分
* @throws Exception 如果加密失敗或身份證號(hào)格式不正確
*/
public IdCardEncryptionResult encryptIdCard(String idCard) throws Exception {
if (idCard == null || idCard.length() != 18) {
throw new IllegalArgumentException("身份證號(hào)必須是18位");
}
// 提取核心部分:前6位 + 后4位
String corePart = idCard.substring(0, 6) + idCard.substring(14);
// 加密核心部分
String encryptedPart = encrypt(corePart);
// 保留中間部分作為索引:第7-14位(出生日期)
String indexPart = idCard.substring(6, 14);
return new IdCardEncryptionResult(encryptedPart, indexPart);
}
/**
* 解密身份證號(hào)
*
* @param encryptedPart 加密的核心部分
* @param indexPart 明文索引部分(出生日期)
* @return 完整的身份證號(hào)
* @throws Exception 如果解密失敗
*/
public String decryptIdCard(String encryptedPart, String indexPart) throws Exception {
if (encryptedPart == null || encryptedPart.isEmpty()) {
throw new IllegalArgumentException("加密部分不能為空");
}
if (indexPart == null || indexPart.length() != 8) {
throw new IllegalArgumentException("索引部分必須是8位(出生日期)");
}
// 解密核心部分
String decryptedCore = decrypt(encryptedPart);
if (decryptedCore.length() != 10) {
throw new IllegalArgumentException("解密后的核心部分長(zhǎng)度不正確");
}
// 組合完整身份證號(hào):前6位 + 中間8位(索引) + 后4位
return decryptedCore.substring(0, 6) + indexPart + decryptedCore.substring(6);
}
/**
* 部分加密手機(jī)號(hào)
* 加密前3位(運(yùn)營(yíng)商號(hào)段)+ 后4位(用戶號(hào)),保留中間部分作為索引
*
* @param phone 手機(jī)號(hào)(11位)
* @return 加密結(jié)果對(duì)象
* @throws Exception 如果加密失敗或手機(jī)號(hào)格式不正確
*/
public PhoneEncryptionResult encryptPhone(String phone) throws Exception {
if (phone == null || phone.length() != 11) {
throw new IllegalArgumentException("手機(jī)號(hào)必須是11位");
}
// 提取核心部分:前3位 + 后4位
String corePart = phone.substring(0, 3) + phone.substring(7);
// 加密核心部分
String encryptedPart = encrypt(corePart);
// 保留中間部分作為索引:第4-7位
String indexPart = phone.substring(3, 7);
return new PhoneEncryptionResult(encryptedPart, indexPart);
}
/**
* 解密手機(jī)號(hào)
*
* @param encryptedPart 加密的核心部分
* @param indexPart 明文索引部分
* @return 完整的手機(jī)號(hào)
* @throws Exception 如果解密失敗
*/
public String decryptPhone(String encryptedPart, String indexPart) throws Exception {
if (encryptedPart == null || encryptedPart.isEmpty()) {
throw new IllegalArgumentException("加密部分不能為空");
}
if (indexPart == null || indexPart.length() != 4) {
throw new IllegalArgumentException("索引部分必須是4位");
}
// 解密核心部分
String decryptedCore = decrypt(encryptedPart);
if (decryptedCore.length() != 7) {
throw new IllegalArgumentException("解密后的核心部分長(zhǎng)度不正確");
}
// 組合完整手機(jī)號(hào):前3位 + 中間4位(索引) + 后4位
return decryptedCore.substring(0, 3) + indexPart + decryptedCore.substring(3);
}
/**
* 身份證號(hào)加密結(jié)果
*/
@Getter
public static class IdCardEncryptionResult {
private final String encryptedPart; // 加密的核心部分(Base64編碼)
private final String indexPart; // 明文索引部分(出生日期,8位)
public IdCardEncryptionResult(String encryptedPart, String indexPart) {
this.encryptedPart = encryptedPart;
this.indexPart = indexPart;
}
@Override
public String toString() {
return String.format("IdCardEncryptionResult{encryptedPart='%s', indexPart='%s'}",
encryptedPart, indexPart);
}
}
/**
* 手機(jī)號(hào)加密結(jié)果
*/
@Getter
public static class PhoneEncryptionResult {
private final String encryptedPart; // 加密的核心部分(Base64編碼)
private final String indexPart; // 明文索引部分(4位)
public PhoneEncryptionResult(String encryptedPart, String indexPart) {
this.encryptedPart = encryptedPart;
this.indexPart = indexPart;
}
@Override
public String toString() {
return String.format("PhoneEncryptionResult{encryptedPart='%s', indexPart='%s'}",
encryptedPart, indexPart);
}
}
}
2.3 測(cè)試用例
增加AES部分加密測(cè)試用例:
/**
* AES部分加密測(cè)試
* @param args
* @author senfel
* @date 2026/2/12 15:33
* @return void
*/
public static void main(String[] args) {
try {
// 創(chuàng)建AES加密服務(wù)
AESPartialEncryption aesService = new AESPartialEncryption("1234567890123456");
System.out.println("=== 測(cè)試身份證號(hào)部分加密 ===");
String idCard = "110101199001011234";
System.out.println("原始身份證號(hào): " + idCard);
// 加密
IdCardEncryptionResult idCardResult = aesService.encryptIdCard(idCard);
//加密結(jié)果: IdCardEncryptionResult{encryptedPart='8FiTU/zAnjzz4fJey48+Fw==', indexPart='19900101'}
//加密的核心部分: 8FiTU/zAnjzz4fJey48+Fw==
//明文索引部分(出生日期): 19900101
System.out.println("加密結(jié)果: " + idCardResult);
System.out.println("加密的核心部分: " + idCardResult.getEncryptedPart());
System.out.println("明文索引部分(出生日期): " + idCardResult.getIndexPart());
// 解密
String decryptedIdCard = aesService.decryptIdCard(
idCardResult.getEncryptedPart(),
idCardResult.getIndexPart()
);
//解密后的身份證號(hào): 110101199001011234
//加密解密是否成功: true
System.out.println("解密后的身份證號(hào): " + decryptedIdCard);
System.out.println("加密解密是否成功: " + idCard.equals(decryptedIdCard));
System.out.println("\n=== 測(cè)試手機(jī)號(hào)部分加密 ===");
String phone = "13800138000";
System.out.println("原始手機(jī)號(hào): " + phone);
// 加密
PhoneEncryptionResult phoneResult = aesService.encryptPhone(phone);
//加密結(jié)果: PhoneEncryptionResult{encryptedPart='Dhf5t5O0Tn1sSwUKB/WKyg==', indexPart='0013'}
//加密的核心部分: Dhf5t5O0Tn1sSwUKB/WKyg==
//明文索引部分: 0013
System.out.println("加密結(jié)果: " + phoneResult);
System.out.println("加密的核心部分: " + phoneResult.getEncryptedPart());
System.out.println("明文索引部分: " + phoneResult.getIndexPart());
// 解密
String decryptedPhone = aesService.decryptPhone(
phoneResult.getEncryptedPart(),
phoneResult.getIndexPart()
);
//解密后的手機(jī)號(hào): 13800138000
//加密解密是否成功: true
System.out.println("解密后的手機(jī)號(hào): " + decryptedPhone);
System.out.println("加密解密是否成功: " + phone.equals(decryptedPhone));
System.out.println("\n=== 測(cè)試通用加密解密 ===");
String plaintext = "HelloWorld123456";
System.out.println("原始文本: " + plaintext);
String encrypted = aesService.encrypt(plaintext);
//加密后: d0Ae9K3M+jCUTX227btKawUBh6DN5amHLLqwkatz5VM=
System.out.println("加密后: " + encrypted);
String decrypted = aesService.decrypt(encrypted);
//解密后: HelloWorld123456
//加密解密是否成功: true
System.out.println("解密后: " + decrypted);
System.out.println("加密解密是否成功: " + plaintext.equals(decrypted));
} catch (Exception e) {
System.err.println("測(cè)試失敗: " + e.getMessage());
e.printStackTrace();
}
}
3 可搜索加密(Searchable Encryption)
3.1 理論基礎(chǔ)
加密特點(diǎn):
- 支持對(duì)文檔進(jìn)行加密,同時(shí)生成可搜索的索引
- 支持在不解密文檔的情況下,通過關(guān)鍵詞進(jìn)行搜索
- 使用對(duì)稱加密(AES)和HMAC實(shí)現(xiàn)可搜索加密
實(shí)現(xiàn)原理:
加密文檔時(shí),提取關(guān)鍵詞并為每個(gè)關(guān)鍵詞生成加密的索引token
搜索時(shí),使用關(guān)鍵詞生成trapdoor(陷阱門),與索引token匹配
匹配成功則說明文檔包含該關(guān)鍵詞,無需解密整個(gè)文檔
使用場(chǎng)景:
加密郵件搜索
加密數(shù)據(jù)庫查詢
云存儲(chǔ)中的加密文檔搜索
3.2 完整實(shí)現(xiàn)代碼
可搜索加密SearchableEncryption:
import lombok.Getter;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* 可搜索加密
* @author senfel
* @version 2.0
* @date 2026/2/12
*/
public class SearchableEncryption {
private static final String ALGORITHM = "AES";
private static final String TRANSFORMATION = "AES";
private static final String HMAC_ALGORITHM = "HmacSHA256";
// 用于加密文檔的密鑰
private final SecretKeySpec encryptionKey;
// 用于生成索引的密鑰
private final SecretKeySpec indexKey;
/**
* 構(gòu)造函數(shù)
* @param encryptionKey 加密密鑰(16、24或32字節(jié))
* @param indexKey 索引密鑰(用于生成搜索索引)
* @throws IllegalArgumentException 如果密鑰長(zhǎng)度不正確
*/
public SearchableEncryption(String encryptionKey, String indexKey) {
if (encryptionKey == null || encryptionKey.isEmpty()) {
throw new IllegalArgumentException("加密密鑰不能為空");
}
if (indexKey == null || indexKey.isEmpty()) {
throw new IllegalArgumentException("索引密鑰不能為空");
}
byte[] encKeyBytes = encryptionKey.getBytes(StandardCharsets.UTF_8);
byte[] idxKeyBytes = indexKey.getBytes(StandardCharsets.UTF_8);
validateKeyLength(encKeyBytes.length, "加密密鑰");
validateKeyLength(idxKeyBytes.length, "索引密鑰");
this.encryptionKey = new SecretKeySpec(encKeyBytes, ALGORITHM);
this.indexKey = new SecretKeySpec(idxKeyBytes, HMAC_ALGORITHM);
}
/**
* 使用默認(rèn)密鑰創(chuàng)建實(shí)例
* @return SearchableEncryption實(shí)例
*/
public static SearchableEncryption createDefault() {
return new SearchableEncryption(
"1234567890123456", // 16字節(jié)加密密鑰
"abcdefghijklmnop" // 16字節(jié)索引密鑰
);
}
/**
* 驗(yàn)證密鑰長(zhǎng)度
*/
private void validateKeyLength(int length, String keyName) {
if (length != 16 && length != 24 && length != 32) {
throw new IllegalArgumentException(
String.format("%s長(zhǎng)度必須是16、24或32字節(jié),當(dāng)前長(zhǎng)度:%d", keyName, length)
);
}
}
/**
* 加密文檔
* @param plaintext 明文文檔
* @param keywords 關(guān)鍵詞列表(用于生成搜索索引)
* @return 加密結(jié)果,包含加密文檔和索引token列表
* @throws Exception 如果加密失敗
*/
public EncryptionResult encrypt(String plaintext, Set<String> keywords) throws Exception {
if (plaintext == null || plaintext.isEmpty()) {
throw new IllegalArgumentException("明文不能為空");
}
if (keywords == null || keywords.isEmpty()) {
keywords = extractKeywords(plaintext);
}
// 加密文檔
String encryptedDocument = encryptDocument(plaintext);
// 為每個(gè)關(guān)鍵詞生成索引token
List<String> indexTokens = new ArrayList<>();
for (String keyword : keywords) {
String token = generateIndexToken(keyword);
indexTokens.add(token);
}
return new EncryptionResult(encryptedDocument, indexTokens);
}
/**
* 加密文檔(不提取關(guān)鍵詞,需要手動(dòng)提供)
* @param plaintext 明文文檔
* @return Base64編碼的密文
* @throws Exception 如果加密失敗
*/
public String encryptDocument(String plaintext) throws Exception {
if (plaintext == null || plaintext.isEmpty()) {
return plaintext;
}
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, encryptionKey);
byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8);
byte[] encryptedBytes = cipher.doFinal(plaintextBytes);
return Base64.getEncoder().encodeToString(encryptedBytes);
}
/**
* 解密文檔
* @param encryptedDocument Base64編碼的密文
* @return 解密后的明文
* @throws Exception 如果解密失敗
*/
public String decryptDocument(String encryptedDocument) throws Exception {
if (encryptedDocument == null || encryptedDocument.isEmpty()) {
return encryptedDocument;
}
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, encryptionKey);
byte[] encryptedBytes = Base64.getDecoder().decode(encryptedDocument);
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
return new String(decryptedBytes, StandardCharsets.UTF_8);
}
/**
* 為關(guān)鍵詞生成索引token
* 使用HMAC生成確定性的token,相同關(guān)鍵詞總是生成相同的token
* @param keyword 關(guān)鍵詞
* @return Base64編碼的索引token
* @throws Exception 如果生成失敗
*/
public String generateIndexToken(String keyword) throws Exception {
if (keyword == null || keyword.isEmpty()) {
throw new IllegalArgumentException("關(guān)鍵詞不能為空");
}
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
mac.init(indexKey);
byte[] keywordBytes = keyword.toLowerCase().trim().getBytes(StandardCharsets.UTF_8);
byte[] tokenBytes = mac.doFinal(keywordBytes);
return Base64.getEncoder().encodeToString(tokenBytes);
}
/**
* 生成搜索陷阱門(Trapdoor)
* 與索引token使用相同的生成方法,確??梢云ヅ?
*
* @param keyword 搜索關(guān)鍵詞
* @return Base64編碼的trapdoor
* @throws Exception 如果生成失敗
*/
public String generateTrapdoor(String keyword) throws Exception {
// Trapdoor與IndexToken使用相同的生成方法
return generateIndexToken(keyword);
}
/**
* 搜索加密文檔
* 通過比較trapdoor和索引token來判斷文檔是否包含關(guān)鍵詞
*
* @param indexTokens 文檔的索引token列表
* @param trapdoor 搜索關(guān)鍵詞的trapdoor
* @return 如果文檔包含該關(guān)鍵詞返回true
*/
public boolean search(List<String> indexTokens, String trapdoor) {
if (indexTokens == null || indexTokens.isEmpty() || trapdoor == null || trapdoor.isEmpty()) {
return false;
}
// 直接比較trapdoor和索引token
return indexTokens.contains(trapdoor);
}
/**
* 批量搜索多個(gè)關(guān)鍵詞
*
* @param indexTokens 文檔的索引token列表
* @param keywords 搜索關(guān)鍵詞列表
* @return 匹配的關(guān)鍵詞集合
* @throws Exception 如果生成trapdoor失敗
*/
public Set<String> searchMultiple(List<String> indexTokens, Set<String> keywords) throws Exception {
Set<String> matchedKeywords = new HashSet<>();
if (indexTokens == null || indexTokens.isEmpty() || keywords == null || keywords.isEmpty()) {
return matchedKeywords;
}
for (String keyword : keywords) {
String trapdoor = generateTrapdoor(keyword);
if (search(indexTokens, trapdoor)) {
matchedKeywords.add(keyword);
}
}
return matchedKeywords;
}
/**
* 從文本中提取關(guān)鍵詞(簡(jiǎn)單實(shí)現(xiàn))
* 實(shí)際應(yīng)用中可以使用更復(fù)雜的分詞和停用詞過濾
*
* @param text 文本內(nèi)容
* @return 關(guān)鍵詞集合
*/
private Set<String> extractKeywords(String text) {
Set<String> keywords = new HashSet<>();
if (text == null || text.isEmpty()) {
return keywords;
}
// 簡(jiǎn)單的關(guān)鍵詞提?。喊纯崭窈蜆?biāo)點(diǎn)符號(hào)分割
String[] words = text.toLowerCase()
.replaceAll("[^\\p{L}\\p{N}\\s]", " ")
.split("\\s+");
// 過濾掉太短的詞(少于2個(gè)字符)
for (String word : words) {
if (word.length() >= 2) {
keywords.add(word.trim());
}
}
return keywords;
}
/**
* 加密結(jié)果類
*/
@Getter
public static class EncryptionResult {
private final String encryptedDocument; // 加密后的文檔(Base64)
private final List<String> indexTokens; // 索引token列表
public EncryptionResult(String encryptedDocument, List<String> indexTokens) {
this.encryptedDocument = encryptedDocument;
this.indexTokens = indexTokens != null ? new ArrayList<>(indexTokens) : new ArrayList<>();
}
@Override
public String toString() {
return String.format("EncryptionResult{encryptedDocument='%s', indexTokens=%d}",
encryptedDocument.substring(0, Math.min(50, encryptedDocument.length())),
indexTokens.size());
}
}
}
3.3 測(cè)試用例
可搜索加密測(cè)試用例:
/**
* 測(cè)試可搜索加密
* @param args
* @author senfel
* @date 2026/2/12 15:45
* @return void
*/
public static void main(String[] args) {
try {
// 創(chuàng)建可搜索加密服務(wù)
SearchableEncryption se = SearchableEncryption.createDefault();
System.out.println("=== 測(cè)試可搜索加密 ===");
// 測(cè)試文檔
String document = "這是一份關(guān)于Java加密技術(shù)的文檔。文檔介紹了AES加密、RSA加密和可搜索加密等技術(shù)。";
System.out.println("原始文檔: " + document);
// 提取關(guān)鍵詞
Set<String> keywords = new HashSet<>(Arrays.asList("Java", "加密", "AES", "RSA", "技術(shù)"));
System.out.println("關(guān)鍵詞: " + keywords);
// 加密文檔并生成索引
EncryptionResult result = se.encrypt(document, keywords);
//加密結(jié)果: EncryptionResult{encryptedDocument='VWEdDDd2SgA7UFWDvuenNphVrloKhQGPWveLfiQOqtjtoSBY9b', indexTokens=5}
//索引token數(shù)量: 5
System.out.println("加密結(jié)果: " + result);
System.out.println("索引token數(shù)量: " + result.getIndexTokens().size());
// 解密文檔驗(yàn)證
String decrypted = se.decryptDocument(result.getEncryptedDocument());
//解密后的文檔: 這是一份關(guān)于Java加密技術(shù)的文檔。文檔介紹了AES加密、RSA加密和可搜索加密等技術(shù)。
//解密是否成功: true
System.out.println("解密后的文檔: " + decrypted);
System.out.println("解密是否成功: " + document.equals(decrypted));
System.out.println("\n=== 測(cè)試搜索功能 ===");
// 搜索單個(gè)關(guān)鍵詞
String searchKeyword = "Java";
String trapdoor = se.generateTrapdoor(searchKeyword);
//搜索關(guān)鍵詞: Java
//生成的Trapdoor: PIc6gWBjjkhSYi5sCQq4cf18M3HHOpg494+wwZQZyeA=
System.out.println("搜索關(guān)鍵詞: " + searchKeyword);
System.out.println("生成的Trapdoor: " + trapdoor);
boolean found = se.search(result.getIndexTokens(), trapdoor);
//是否找到關(guān)鍵詞 'Java': true
System.out.println("是否找到關(guān)鍵詞 '" + searchKeyword + "': " + found);
// 搜索不存在的關(guān)鍵詞
String notFoundKeyword = "Python";
String notFoundTrapdoor = se.generateTrapdoor(notFoundKeyword);
boolean notFound = se.search(result.getIndexTokens(), notFoundTrapdoor);
//是否找到關(guān)鍵詞 'Python': false
System.out.println("是否找到關(guān)鍵詞 '" + notFoundKeyword + "': " + notFound);
// 批量搜索
Set<String> searchKeywords = new HashSet<>(Arrays.asList("Java", "加密", "Python", "AES"));
Set<String> matched = se.searchMultiple(result.getIndexTokens(), searchKeywords);
System.out.println("\n批量搜索結(jié)果:");
//搜索關(guān)鍵詞: [Java, 加密, Python, AES]
//匹配的關(guān)鍵詞: [Java, 加密, AES]
System.out.println("搜索關(guān)鍵詞: " + searchKeywords);
System.out.println("匹配的關(guān)鍵詞: " + matched);
System.out.println("\n=== 測(cè)試自動(dòng)提取關(guān)鍵詞 ===");
String document2 = "Spring Boot是一個(gè)優(yōu)秀的Java框架,支持RESTful API開發(fā)。";
// 自動(dòng)提取關(guān)鍵詞
EncryptionResult result2 = se.encrypt(document2, null);
//文檔2: Spring Boot是一個(gè)優(yōu)秀的Java框架,支持RESTful API開發(fā)。
//自動(dòng)提取的索引token數(shù)量: 4
System.out.println("文檔2: " + document2);
System.out.println("自動(dòng)提取的索引token數(shù)量: " + result2.getIndexTokens().size());
// 搜索
String trapdoor2 = se.generateTrapdoor("Spring");
boolean found2 = se.search(result2.getIndexTokens(), trapdoor2);
//是否找到 'Spring': true
System.out.println("是否找到 'Spring': " + found2);
} catch (Exception e) {
System.err.println("測(cè)試失敗: " + e.getMessage());
e.printStackTrace();
}
}
總結(jié)
Spring Boot環(huán)境下實(shí)現(xiàn)加密字段模糊查詢需要在安全性和性能之間找到平衡。本文介紹了三種主流方案:
1.格式保留加密:適用于需要保持?jǐn)?shù)據(jù)格式的場(chǎng)景,實(shí)現(xiàn)復(fù)雜但安全性高
2.部分加密與明文索引:平衡安全與性能,適用于大多數(shù)業(yè)務(wù)場(chǎng)景
3.可搜索加密:支持密文直接搜索,性能開銷較大但安全性最高
在實(shí)際項(xiàng)目中,應(yīng)根據(jù)業(yè)務(wù)需求和安全要求選擇合適的方案。對(duì)于大多數(shù)企業(yè)級(jí)應(yīng)用,部分加密與明文索引是一個(gè)不錯(cuò)的選擇,它在保證核心數(shù)據(jù)安全的同時(shí),能夠提供較好的查詢性能。
未來,隨著同態(tài)加密技術(shù)的成熟和性能提升,可能會(huì)成為加密字段模糊查詢的終極解決方案。但在當(dāng)前階段,上述三種方案仍然是最實(shí)用的選擇。
以上就是SpringBoot實(shí)現(xiàn)加密字段模糊查詢的最佳實(shí)踐的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot加密字段模糊查詢的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
使用工具類-java精確到小數(shù)點(diǎn)后6位
這篇文章主要介紹了使用工具類-java精確到小數(shù)點(diǎn)后6位,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10
Spring的@CrossOrigin注解處理請(qǐng)求源碼解析
這篇文章主要介紹了Spring的@CrossOrigin注解處理請(qǐng)求源碼解析,@CrossOrigin源碼解析主要分為兩個(gè)階段@CrossOrigin注釋的方法掃描注冊(cè),請(qǐng)求匹配@CrossOrigin注釋的方法,本文從源碼角度進(jìn)行解析,需要的朋友可以參考下2023-12-12
MyBatis動(dòng)態(tài)SQL標(biāo)簽用法實(shí)例詳解
本文通過實(shí)例代碼給大家介紹了MyBatis動(dòng)態(tài)SQL標(biāo)簽用法,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下吧2017-07-07
Java實(shí)現(xiàn)調(diào)用jython執(zhí)行python文件的方法
這篇文章主要介紹了Java實(shí)現(xiàn)調(diào)用jython執(zhí)行python文件的方法,結(jié)合實(shí)例形式分析了Java調(diào)用jython執(zhí)行python文件的常見操作技巧及相關(guān)問題解決方法,需要的朋友可以參考下2018-03-03
springboot之Validation參數(shù)校驗(yàn)詳細(xì)解讀
這篇文章主要介紹了springboot之Validation參數(shù)校驗(yàn)詳細(xì)解讀,本篇是關(guān)于springboot的參數(shù)校驗(yàn)知識(shí),當(dāng)然也適用其它java應(yīng)用,讀完本篇將學(xué)會(huì)基本的參數(shù)校驗(yàn),自定義參數(shù)校驗(yàn)和分組參數(shù)校驗(yàn),需要的朋友可以參考下2023-10-10
Spring Boot 動(dòng)態(tài)多數(shù)據(jù)源核心思路與關(guān)鍵介紹
這篇文章給大家介紹了Spring Boot 動(dòng)態(tài)多數(shù)據(jù)源核心思路與關(guān)鍵介紹,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2026-03-03
BeanFactory與ApplicationContext的區(qū)別示例解析
這篇文章主要為大家介紹了BeanFactory與ApplicationContext的區(qū)別示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11
Java兩種方式實(shí)現(xiàn)動(dòng)態(tài)代理
Java 在 java.lang.reflect 包中有自己的代理支持,該類(Proxy.java)用于動(dòng)態(tài)生成代理類,只需傳入目標(biāo)接口、目標(biāo)接口的類加載器以及 InvocationHandler 便可為目標(biāo)接口生成代理類及代理對(duì)象。我們稱這個(gè)Java技術(shù)為:動(dòng)態(tài)代理2020-10-10
Spring AOP面向切面編程實(shí)現(xiàn)原理方法詳解
這篇文章主要介紹了Spring AOP面向切面編程實(shí)現(xiàn)原理方法詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08

