使用 Jasypt 自訂偵測及解密方法
使用 Jasypt-spring-boot 自訂 解密器(Custom Encryptor) 之後,不論是預設的 PBE(Password-based Encryption),
或是 v2.1.1 版所支援的 非對稱式加密(Asymmetric Encryption),都會轉導至自訂的解密器進行處理,
以下教學如何讓自訂的解密器只在使用其中一種加密方式時生效。
1. Gradle 設定
plugins {
id 'org.springframework.boot' version '2.7.1'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.thinkpower'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:2.1.2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
2. application.properties 設定
在此範例中,我先將兩種演算法需要使用到的項目先準備好 (加密的方式可以參考這裡)。
# 使用 PBE 時的設定
jasypt.encryptor.password=123456
# 使用 PBE 所加密的字串
password=ENC(pm+1TlPBLU4R/USxTnC8OQ==)
# 使用 Asymmetric Encryption 時的設定
jasypt.encryptor.privateKeyFormat=PEM
jasypt.encryptor.privateKeyString=\
-----BEGIN PRIVATE KEY-----\
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCtB/IYK8E52CYMZTpyIY9U0HqMewyKnRvSo6s+9VNIn/HSh9+MoBGiADa2MaPKvetS3CD3CgwGq/+L\
IQ1HQYGchRrSORizOcIp7KBx+Wc1riatV/tcpcuFLC1j6QJ7d2I+T7RA98Sx8X39orqlYFQVysTw/aTawX/yajx0UlTW3rNAY+ykeQ0CBHowtTxKM9nGcxLoQbvbYx1i\
G9JgAqye7TYejOpviOH+BpD8To2S8zcOSojIhixEfayay0gURv0IKJN2LP86wkpAuAbL+mohUq1qLeWdTEBrIRXjlnrWs1M66w0l/6JwaFnGOqEB6haMzE4JWZULYYpr\
2yKyoGCRAgMBAAECggEAQxURhs1v3D0wgx27ywO3zeoFmPEbq6G9Z6yMd5wk7cMUvcpvoNVuAKCUlY4pMjDvSvCM1znN78g/CnGF9FoxJb106Iu6R8HcxOQ4T/ehS+54\
kDvL999PSBIYhuOPUs62B/Jer9FfMJ2veuXb9sGh19EFCWlMwILEV/dX+MDyo1qQaNzbzyyyaXP8XDBRDsvPL6fPxL4r6YHywfcPdBfTc71/cEPksG8ts6um8uAVYbLI\
DYcsWopjVZY/nUwsz49xBCyRcyPnlEUJedyF8HANfVEO2zlSyRshn/F+rrjD6aKBV/yVWfTEyTSxZrBPl4I4Tv89EG5CwuuGaSagxfQpAQKBgQDXEe7FqXSaGk9xzuPa\
zXy8okCX5pT6545EmqTP7/JtkMSBHh/xw8GPp+JfrEJEAJJl/ISbdsOAbU+9KAXuPmkicFKbodBtBa46wprGBQ8XkR4JQoBFj1SJf7Gj9ozmDycozO2Oy8a1QXKhHUPk\
bPQ0+w3efwoYdfE67ZodpFNhswKBgQDN9eaYrEL7YyD7951WiK0joq0BVBLK3rwO5+4g9IEEQjhP8jSo1DP+zS495t5ruuuuPsIeodA79jI8Ty+lpYqqCGJTE6muqLMJ\
Diy7KlMpe0NZjXrdSh6edywSz3YMX1eAP5U31pLk0itMDTf2idGcZfrtxTLrpRffumowdJ5qqwKBgF+XZ+JRHDN2aEM0atAQr1WEZGNfqG4Qx4o0lfaaNs1+H+knw5kI\
ohrAyvwtK1LgUjGkWChlVCXb8CoqBODMupwFAqKL/IDImpUhc/t5uiiGZqxE85B3UWK/7+vppNyIdaZL13a1mf9sNI/p2whHaQ+3WoW/P3R5z5uaifqM1EbDAoGAN584\
JnUnJcLwrnuBx1PkBmKxfFFbPeSHPzNNsSK3ERJdKOINbKbaX+7DlT4bRVbWvVj/jcw/c2Ia0QTFpmOdnivjefIuehffOgvU8rsMeIBsgOvfiZGx0TP3+CCFDfRVqjIB\
t3HAfAFyZfiP64nuzOERslL2XINafjZW5T0pZz8CgYAJ3UbEMbKdvIuK+uTl54R1Vt6FO9T5bgtHR4luPKoBv1ttvSC6BlalgxA0Ts/AQ9tCsUK2JxisUcVgMjxBVvG0\
lfq/EHpL0Wmn59SHvNwtHU2qx3Ne6M0nQtneCCfR78OcnqQ7+L+3YCMqYGJHNFSard+dewfKoPnWw0WyGFEWCg==\
-----END PRIVATE KEY-----
# 使用 Asymmetric Encryption 所加密的字串
#password=ENC(Jg2DISMYfFQbQKTCK4xLjXhtDc+cF8oU6Ha7ohU6Tal06942tr6psxmGuCnrWDh32YhGIB2qwI0KVtkR2RyhUuEHFycit673gI7ioXiLkLcH4TFJN3OV7es02O8GZWy+Thdnv0GMdIfZLp2ywi5lkCD0VEqhLX7wKxXUAwRrMZPlkhn/fE/eQP+mx0bw/mXYOleaxcrqm85c3rPLT8j3C+rpLCr1EWKXnPMCmU/bU9FeXCOBkpTGow/sWZgKKXdqb50yCdCXIAr3KiFaQbSI/EkAKJKHQlf7R2jyfcjrBUOhJAdmU2SU0ijzhinRUpRA3dtRtviYpPqbfigh6qHN0g==)
3. 實作 EncryptablePropertyResolver 介面,定義如何 偵測(detecting) 與 解密(decrypting)
import com.ulisesbocchio.jasyptspringboot.EncryptablePropertyResolver;
public class CustomEncryptablePropertyResolver implements EncryptablePropertyResolver {
@Override
public String resolvePropertyValue(String value) {
// value 包含識別字 "ENC", 如: "ENC(pm+1TlPBLU4R/USxTnC8OQ==)
// TODO 將 value 解密之後再回傳
return value;
}
}
我們的目標是希望在 PBE 模式下,使用預設的解密方式,但在 Asymmetric Encryption 模式下使用自訂的演算法,所以需要保留預設的加密器 (StringEncryptor),並用它來實現 resolvePropertValue 方法。
import org.jasypt.encryption.StringEncryptor;
import com.ulisesbocchio.jasyptspringboot.EncryptablePropertyResolver;
public class CustomEncryptablePropertyResolver implements EncryptablePropertyResolver {
private final StringEncryptor stringEncryptor;
public CustomEncryptablePropertyResolver(StringEncryptor defaultStringEncryptor) {
this.stringEncryptor = defaultStringEncryptor;
}
@Override
public String resolvePropertyValue(String value) {
if (value != null && value.startsWith("ENC(")) {
// 先拆解出識別字中被加密的字段
String encrypted = value.substring(value.indexOf("ENC(") + 4, value.lastIndexOf(")"));
// 使用預設加密器進行解密
String decrypted = this.stringEncryptor.decrypt(encrypted);
return decrypted;
}
return value;
}
}
4. 註冊客製的 CustomEncryptablePropertyResolver 類別到 Spring Context
※ 注意:一旦註冊了 "encryptablePropertyResolver" 這個名稱的 Bean,則不論 PBE 或 非對稱式,一律都交由自訂的 Bean 處理加解密
import com.thinkpower.jasypt.component.CustomEncryptablePropertyResolver;
import com.ulisesbocchio.jasyptspringboot.EncryptablePropertyResolver;
import com.ulisesbocchio.jasyptspringboot.encryptor.DefaultLazyEncryptor;
@Configuration
public class JasyptConfig {
@Autowired
private DefaultLazyEncryptor defaultStringEncryptor;
@Bean(name="encryptablePropertyResolver")
public EncryptablePropertyResolver encryptablePropertyResolver() {
// 建構時,傳入預設的 StringEncryptor
return new CustomEncryptablePropertyResolver(defaultStringEncryptor);
}
}
因此我們需要讀取 properties 檔,來判斷使用自訂解密演算法的時機,可以透過 JasyptEncryptorConfigurationProperties 類別來取得 jasypt-spring-boot 相關的設定值。
import com.thinkpower.jasypt.component.CustomEncryptablePropertyResolver;
import com.ulisesbocchio.jasyptspringboot.EncryptablePropertyResolver;
import com.ulisesbocchio.jasyptspringboot.encryptor.DefaultLazyEncryptor;
import com.ulisesbocchio.jasyptspringboot.properties.JasyptEncryptorConfigurationProperties;
import com.ulisesbocchio.jasyptspringboot.util.Singleton;
@Configuration
public class JasyptConfig {
@Autowired
private DefaultLazyEncryptor defaultStringEncryptor;
// 關於 jasypt 的設定值 (from application-*.properties)
@Autowired
private Singleton<JasyptEncryptorConfigurationProperties> configPropsSingleton;
@Bean(name="encryptablePropertyResolver")
public EncryptablePropertyResolver encryptablePropertyResolver() {
// 建構時,傳入預設的 StringEncryptor
return new CustomEncryptablePropertyResolver(defaultStringEncryptor, configPropsSingleton);
}
}
5. 修改 CustomEncryptablePropertyResolver 類別
jasypt-spring-boot 預設會先判斷 properties 中是否有 "jasypt.encryptor.password" 項目,若有,則一律使用 PBE 作為加解密演算法;若沒有,才會再判斷是否有 "jasypt.encryptor.private-key-string" 或 "jasypt.encryptor.private-key-location" 項目,若有,才會使用 Asymmetric Encryption 演算法。
我們參考預設的邏輯,並實作自定義的 加密(encrypt) 與 解密(decrypt) 方法:
import org.jasypt.encryption.StringEncryptor;
import org.springframework.util.ObjectUtils;
import com.ulisesbocchio.jasyptspringboot.EncryptablePropertyResolver;
import com.ulisesbocchio.jasyptspringboot.properties.JasyptEncryptorConfigurationProperties;
import com.ulisesbocchio.jasyptspringboot.util.Singleton;
public class CustomEncryptablePropertyResolver implements EncryptablePropertyResolver {
private final StringEncryptor stringEncryptor;
public CustomEncryptablePropertyResolver(StringEncryptor defaultStringEncryptor, //
Singleton<JasyptEncryptorConfigurationProperties> configProps) {
StringEncryptor encryptor = null;
try {
// 取得 properties 的設定值
String password = configProps.get().getPassword();
String privateKeyLocation = configProps.get().getPrivateKeyLocation();
String privateKeyString = configProps.get().getPrivateKeyString();
// 有設定 'jasypt.encryptor.password' 就走 PBE
if (!ObjectUtils.isEmpty(password)) {
System.out.println("Password-based Encryption Configuration detected!");
encryptor = defaultStringEncryptor;
// 有設定 "jasypt.encryptor.private-key-string" 或 "jasypt.encryptor.private-key-location" 就走非對稱式
} else if (!ObjectUtils.isEmpty(privateKeyString) || !ObjectUtils.isEmpty(privateKeyLocation)) {
System.out.println("Asymmetric Encryption Configuration detected!");
encryptor = new StringEncryptor() {
@Override
public String encrypt(String message) {
// 主要目標是解密, 所以加密用預設的演算法即可
return defaultStringEncryptor.encrypt(message);
}
@Override
public String decrypt(String encryptedMessage) {
// 使用預設演算法解密後, 故意在字串後面加上 "_TPI_FOREVER" 字樣,以利測試
String decryptedMessage = defaultStringEncryptor.decrypt(encryptedMessage);
decryptedMessage += "_TPI_FOREVER";
return decryptedMessage;
}
};
} else {
throw new Exception("Either 'jasypt.encryptor.password' or"
+ " one of ['jasypt.encryptor.private-key-string', 'jasypt.encryptor.private-key-location']"
+ " must be provided for Password-based or Asymmetric encryption");
}
} catch (Exception e) {
e.printStackTrace();
}
this.stringEncryptor = encryptor;
}
@Override
public String resolvePropertyValue(String value) {
if (value != null && value.startsWith("ENC(")) {
// 先拆解出識別字中被加密的字段
String encrypted = value.substring(value.indexOf("ENC(") + 4, value.lastIndexOf(")"));
// 使用預設加密器進行解密
String decrypted = this.stringEncryptor.decrypt(encrypted);
return decrypted;
}
return value;
}
}
6. 建立測試類別
透過 @Value 取得解密後的值
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Value("${password:}")
private String password;
@PostConstruct
public void init() {
System.out.println("Decrypted password: " + password);
}
}
專案結構如下:
7. 驗證:分別透過 PBE 與 Asymmetric Encryption 取得 properties value 的結果
執行 com.thinkpower.jasypt.JasyptApplication.java
7-1. PBE 解密結果:
可以看到解密出來的文字為 "abc",沒有加上 "TPI_FOREVER",表示不是使用自訂的演算法進行解密。
7-2. Asymmetric Encryption 解密結果:
先修改 application.properties,讓非對稱式解密的設定生效:
再執行 JasyptApplication.java
可以看到解密出來的文字為 "abc_TPI_FOREVER",表示是使用自訂的演算法進行解密。
8. 結論
透過實作 jasypt-spring-boot 的 EncryptablePropertyResolver 介面,可以讓我們自訂 偵測(Detecting) 與 解密(Decrypting) 的演算法,並在註冊 Spring Bean 的時候傳入預設的解密器 (StringEncryptor) 與 Jasypt 設定值,可以讓我們自行決定何時採用自訂的演算法。
參考資料: