1. 需求
Nacos中关于中间件的密码,还有第三方API的密钥等信息,都是明文存储,不符合系统安全要求。现需对这些信息进行加密处理,Nacos只存储密文,并在服务启动时,调用云厂商的KMS接口进行解密,将解密后的明文存储在内存中供服务后续使用。
2. 组件调研&增强
2.1. jasypt 组件
业界上已有jasypt
组件可以很好地支持对配置文件中的敏感信息进行解密处理,并完全支持 Spring Boot 架构。但是唯一的缺点是,该组件默认仅支持一些核心的静态加解密算法,无法支持接入第三方KMS;并且,仅支持对一个完整配置项的解密处理,不支持对配置项中某段字符串进行解密操作。因此,不能直接使用在我们已有的服务上。
2.2. 增强
我们需对上述组件进行增强,主要是以下两点:
- 配置项部分解密:支持解密配置项中指定的某段字符串,使用ENC()CNE括住
- 第三方KMS解密:支持接入第三方KMS接口,对密文进行解密操作,支持将明文托管在KMS中(这里使用的是腾讯云的KMS)
我们不但要对jasypt组件进行增强,同时也需在此基础上再封装一个组件
,用于整个平台的服务使用,避免冗余代码。
2.2.1. 引入相关依赖
核心是引入jasypt
的 Spring Boot 组件,以及KMS(如腾讯云)组件:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java</artifactId>
<version>3.1.927</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.16.0</version>
</dependency>
2.2.2. 启动配置解密注解
此注解作为服务引入KMS加密的钥匙,加了才会启动KMS解密功能:
/**
* 开启kms配置密文解密
* @author winfun
* @since 2023-12-18
**/
@Inherited
@Component
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import({KmsConfig.class})
@EnableEncryptableProperties
public @interface EnableKmsPropertiesDecryption {
}
2.2.3. Kms配置类
主要包含上面提到的增强点:
- 增强Detector:用于识别
ENC()CNE
括住的密文 - 增强Encryptor:用于调用KMS的解密接口,返回解密后的明文供服务使用
需要注意的是,默认启用增强代码,如果我们需要临时关闭,可将配置项kms.enabled
设置为false
/**
* kms自动配置
* @author howinfun
* @since 2023/12/15
*/
@Slf4j
@Configuration
@EnableConfigurationProperties({KmsProperties.class})
@AutoConfigureBefore(EnableEncryptablePropertiesConfiguration.class)
@ConditionalOnProperty(name = "kms.enabled",havingValue = "true",matchIfMissing = true)
public class KmsConfig {
private static final String prefix = "ENC(";
private static final String suffix = ")CNE";
@Bean
public KmsClient kmsClient(KmsProperties kmsProperties) {
Assert.notNull(kmsProperties.getSecretId(), "SecretId can't be null");
Assert.notNull(kmsProperties.getSecretKey(), "SecretKey can't be null");
Assert.notNull(kmsProperties.getRegion(), "Region can't be null");
Credential cred = new Credential(kmsProperties.getSecretId(),kmsProperties.getSecretKey());
// 实例化要请求产品的client对象,clientProfile是可选的
return new KmsClient(cred, kmsProperties.getRegion());
}
/**
* 自定义Detector,支持文本中带加密内容
* @return EncryptablePropertyDetector
*/
@Bean("encryptablePropertyDetector")
public EncryptablePropertyDetector encryptablePropertyDetector() {
return new EncryptablePropertyDetector() {
@Override
public boolean isEncrypted(String property) {
if (property == null) {
return false;
} else {
String trimmedValue = property.trim();
boolean isEncrypted = trimmedValue.contains(prefix) && trimmedValue.contains(suffix);
log.debug("收到配置:{},是否需要解密:{}",property,isEncrypted);
return isEncrypted;
}
}
@Override
public String unwrapEncryptedValue(String property) {
return property;
}
};
}
/**
* 自定义Encryptor,支持kms解密
* @return StringEncryptor
*/
@Bean("jasyptStringEncryptor")
public StringEncryptor jasyptStringEncryptor(KmsClient kmsClient) {
return new StringEncryptor() {
@Override
public String encrypt(String message) {
return message;
}
@Override
public String decrypt(String encryptedMessage) {
try {
String trim = encryptedMessage.trim();
String encryptedMsg = trim.substring((trim.indexOf(prefix) + prefix.length()), trim.indexOf(suffix));
String prefixMsg = "";
String suffixMsg = "";
if (!trim.startsWith(prefix) || !trim.endsWith(suffix)) {
prefixMsg = trim.substring(0, trim.indexOf(prefix));
suffixMsg = trim.substring(trim.indexOf(suffix) + suffix.length());
}
DecryptRequest req = new DecryptRequest();
req.setCiphertextBlob(encryptedMsg);
DecryptResponse resp = kmsClient.Decrypt(req);
String pass = new String(Base64.decodeBase64(resp.getPlaintext()));
log.debug("密码解密成功,待处理密文:{},明文:{}", trim, pass);
return prefixMsg + pass + suffixMsg;
} catch (Exception e) {
log.error("密文解密失败", e);
}
return encryptedMessage;
}
};
}
}
2.3.4. Kms配置类
因为调用 KMS 接口,也是需要sk相关信息,但是这里有一个要注意的点,不要写在Nacos等配置中心中,而是以环境变量注入较好。
/**
* kms配置
* @author winfun
* @since 2023/12/18
**/
@Data
@ConfigurationProperties(prefix = "kms")
public class KmsProperties {
private String secretId;
private String secretKey;
private String region;
}
3. 打包组件&使用
我们根据上述代码,可以打包成一个 Spring Boot 的 starter,供后续的所有服务使用。
下面拿我自己本地的作为例子看看:
1、引入KMS组件依赖:
2、加入kms配置:
因为是本地启动调试,就直接先写在配置里头了
3、明文密码替换为密文,并用ENC()CNE
括住
4、应用加入启动KMS解密注解:
5、服务启动成功