原创文章,转载请注明出处
最近在忙脚手架升级,为了减少中间件依赖,降低学习成本,将原来使用的Apollo弃用,和服务发现一同使用 Nacos 来实现。
后面公司安全部门做安全检查,要求对敏感配置增加安全保护,需要实现对部分配置的配置加密。
先说一下版本。
spring-boot-starter-parent: 2.3.11.RELEASE
spring-cloud-starter-alibaba-nacos-discovery: 2.2.6.RELEASE
spring-cloud-starter-alibaba-nacos-config: 2.2.6.RELEASE
查阅Nacos官方文档,配置加密功能当前未支持,所以只好自己码。
我们的目标如下
最开始的尝试是希望依赖于Spring扩展点对数据做加解密处理,我尝试了两个方式
但是经过试验,两个扩展点的切入都是在Nacos将配置加载入Context之前,所以并不适用这次的需求。
也考虑到后期使用Nacos配置热更新的能力,放弃了直接从下层Spring扩展。
Spring扩展失败,只能从更上层的starter想办法。
通过代码定位,可能找到配置加载解析位置是在
com.alibaba.cloud.nacos.client.OvseNacosPropertySourceBuilder的loadNacosData方法中调用com.alibaba.cloud.nacos.parser.NacosDataParserHandler的parseNacosData实现。
我们的目标是尽量不影响后续的版本升级,使用原生包,尽量减少代码的覆盖侵入。
在
spring-cloud-starter-alibaba-nacos-config 中,找到了关键的配置文件。
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true)
public class NacosConfigBootstrapConfiguration {
@Bean
@ConditionalOnMissingBean
public NacosConfigProperties nacosConfigProperties() {
return new NacosConfigProperties();
}
@Bean
@ConditionalOnMissingBean
public NacosConfigManager nacosConfigManager(
NacosConfigProperties nacosConfigProperties) {
return new NacosConfigManager(nacosConfigProperties);
}
@Bean
public NacosPropertySourceLocator nacosPropertySourceLocator(
NacosConfigManager nacosConfigManager) {
return new NacosPropertySourceLocator(nacosConfigManager);
}
}
我们能用的扩展点就是 @ConditionalOnMissingBean 的这两个 Bean。
我尝试重写了NacosConfigManager这个Bean,并在自己脚手架的 spring.factories 中进行了配置,也使用了@Order注解将自定义的Bean置为最高优先级。
但是测试发现由于factories加载顺序问题,自定义的配置类还是晚于Nacos自己的配置加载,导致原生的NacosConfigManager仍会被加载。
所以我只能尝试使用覆盖原生包配置的方法实现。
定义一个跟原包同名的包 com.alibaba.cloud.nacos,并重写配置类,这样会加载到你自定义的配置类。
@Configuration(proxyBeanMethods = false)
public class NacosConfigBootstrapConfiguration {
@Bean
public OvseNacosCipherConfigProcessor ovseNacosCipherConfigProcessor(Environment environment) {
return new OvseNacosCipherConfigProcessor(environment);
}
@Bean
@ConditionalOnMissingBean
public NacosConfigProperties nacosConfigProperties() {
return new NacosConfigProperties();
}
@Bean
@ConditionalOnMissingBean
public NacosConfigManager nacosConfigManager(
NacosConfigProperties nacosConfigProperties) {
return new NacosConfigManager(nacosConfigProperties);
}
@Bean
public OvseNacosPropertySourceLocator nacosPropertySourceLocator(
NacosConfigManager nacosConfigManager, OvseNacosCipherConfigProcessor ovseNacosCipherConfigProcessor) {
return new OvseNacosPropertySourceLocator(nacosConfigManager, ovseNacosCipherConfigProcessor);
}
}
可以看出我们注入了一个自己的密文解析器并替换了
NacosPropertySourceLocator。
密文解析器的实现是从环境变量中根据约定的Key提取加密密钥,我们各环境使用K8s部署,可以方便的管理环境变量和密钥。
@Slf4j
public class OvseNacosCipherConfigProcessor {
private boolean secretAvailable;
private AesEncryptor aesEncryptor;
public static final String SECRET_ENV_PROP_NAME = "OVSE_ENV_SECRET";
public static final String CIPHER_PREFIX = "(ovse-cipher-start)";
public static final String CIPHER_SUFFIX = "(ovse-cipher-end)";
public OvseNacosCipherConfigProcessor(Environment environment) {
String secret = environment.getProperty(SECRET_ENV_PROP_NAME);
this.secretAvailable = StringUtils.isNotBlank(secret);
if (this.secretAvailable) {
try {
this.aesEncryptor = new AesEncryptor(secret);
} catch (Exception e) {
throw new RuntimeException(e);
}
log.info("ovse nacos cipher config enable!");
} else {
log.warn("ovse nacos cipher config unavailable!");
}
}
public String process(String source) {
while (source.contains(CIPHER_PREFIX)) {
int startIndex = source.indexOf(CIPHER_PREFIX);
int endIndex = source.indexOf(CIPHER_SUFFIX);
if (startIndex > endIndex) {
throw new RuntimeException("ovse cipher config end cannot before start: " + source);
}
String cipher = source.substring(startIndex + CIPHER_PREFIX.length(), endIndex);
String plain = cipher2Plain(cipher);
source = source.substring(0, startIndex) + plain + source.substring(endIndex + CIPHER_SUFFIX.length());
}
return source;
}
private String cipher2Plain(String cipher) {
try {
return this.aesEncryptor.decrypt(cipher);
} catch (Exception e) {
throw new RuntimeException("ovse cipher config format error", e);
}
}
}
然后重写了
OvseNacosPropertySourceBuilder和OvseNacosPropertySourceLocator
public class OvseNacosPropertySourceLocator extends NacosPropertySourceLocator {
private static final Logger log = LoggerFactory
.getLogger(NacosPropertySourceLocator.class);
private static final String NACOS_PROPERTY_SOURCE_NAME = "NACOS";
private static final String SEP1 = "-";
private static final String DOT = ".";
private NacosPropertySourceBuilder nacosPropertySourceBuilder;
private NacosConfigProperties nacosConfigProperties;
private NacosConfigManager nacosConfigManager;
private OvseNacosCipherConfigProcessor ovseNacosCipherConfigProcessor;
/**
* recommend to use
* {@link NacosPropertySourceLocator#NacosPropertySourceLocator(com.alibaba.cloud.nacos.NacosConfigManager)}.
* @param nacosConfigProperties nacosConfigProperties
*/
@Deprecated
public OvseNacosPropertySourceLocator(NacosConfigProperties nacosConfigProperties) {
super(nacosConfigProperties);
this.nacosConfigProperties = nacosConfigProperties;
}
public OvseNacosPropertySourceLocator(NacosConfigManager nacosConfigManager, OvseNacosCipherConfigProcessor ovseNacosCipherConfigProcessor) {
super(nacosConfigManager);
this.nacosConfigManager = nacosConfigManager;
this.nacosConfigProperties = nacosConfigManager.getNacosConfigProperties();
this.ovseNacosCipherConfigProcessor = ovseNacosCipherConfigProcessor;
}
@Override
public PropertySource<?> locate(Environment env) {
nacosConfigProperties.setEnvironment(env);
ConfigService configService = nacosConfigManager.getConfigService();
if (null == configService) {
log.warn("no instance of config service found, can't load config from nacos");
return null;
}
long timeout = nacosConfigProperties.getTimeout();
nacosPropertySourceBuilder = new OvseNacosPropertySourceBuilder(configService,
timeout, ovseNacosCipherConfigProcessor);
String name = nacosConfigProperties.getName();
String dataIdPrefix = nacosConfigProperties.getPrefix();
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = name;
}
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = env.getProperty("spring.Application.name");
}
CompositePropertySource composite = new CompositePropertySource(
NACOS_PROPERTY_SOURCE_NAME);
loadSharedConfiguration(composite);
loadExtConfiguration(composite);
loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
return composite;
}
/**
* load shared configuration.
*/
private void loadSharedConfiguration(
CompositePropertySource compositePropertySource) {
List<NacosConfigProperties.Config> sharedConfigs = nacosConfigProperties
.getSharedConfigs();
if (!CollectionUtils.isEmpty(sharedConfigs)) {
checkConfiguration(sharedConfigs, "shared-configs");
loadNacosConfiguration(compositePropertySource, sharedConfigs);
}
}
/**
* load extensional configuration.
*/
private void loadExtConfiguration(CompositePropertySource compositePropertySource) {
List<NacosConfigProperties.Config> extConfigs = nacosConfigProperties
.getExtensionConfigs();
if (!CollectionUtils.isEmpty(extConfigs)) {
checkConfiguration(extConfigs, "extension-configs");
loadNacosConfiguration(compositePropertySource, extConfigs);
}
}
/**
* load configuration of application.
*/
private void loadApplicationConfiguration(
CompositePropertySource compositePropertySource, String dataIdPrefix,
NacosConfigProperties properties, Environment environment) {
String fileExtension = properties.getFileExtension();
String nacosGroup = properties.getGroup();
// load directly once by default
loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
fileExtension, true);
// load with suffix, which have a higher priority than the default
loadNacosDataIfPresent(compositePropertySource,
dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
// Loaded with profile, which have a higher priority than the suffix
for (String profile : environment.getActiveProfiles()) {
String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
fileExtension, true);
}
}
private void loadNacosConfiguration(final CompositePropertySource composite,
List<NacosConfigProperties.Config> configs) {
for (NacosConfigProperties.Config config : configs) {
loadNacosDataIfPresent(composite, config.getDataId(), config.getGroup(),
NacosDataParserHandler.getInstance()
.getFileExtension(config.getDataId()),
config.isRefresh());
}
}
private void checkConfiguration(List<NacosConfigProperties.Config> configs,
String tips) {
for (int i = 0; i < configs.size(); i++) {
String dataId = configs.get(i).getDataId();
if (dataId == null || dataId.trim().length() == 0) {
throw new IllegalStateException(String.format(
"the [ spring.cloud.nacos.config.%s[%s] ] must give a dataId",
tips, i));
}
}
}
private void loadNacosDataIfPresent(final CompositePropertySource composite,
final String dataId, final String group, String fileExtension,
boolean isRefreshable) {
if (null == dataId || dataId.trim().length() < 1) {
return;
}
if (null == group || group.trim().length() < 1) {
return;
}
NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group,
fileExtension, isRefreshable);
this.addFirstPropertySource(composite, propertySource, false);
}
private NacosPropertySource loadNacosPropertySource(final String dataId,
final String group, String fileExtension, boolean isRefreshable) {
if (NacosContextRefresher.getRefreshCount() != 0) {
if (!isRefreshable) {
return NacosPropertySourceRepository.getNacosPropertySource(dataId,
group);
}
}
return nacosPropertySourceBuilder.build(dataId, group, fileExtension,
isRefreshable);
}
/**
* Add the nacos configuration to the first place and maybe ignore the empty
* configuration.
*/
private void addFirstPropertySource(final CompositePropertySource composite,
NacosPropertySource nacosPropertySource, boolean ignoreEmpty) {
if (null == nacosPropertySource || null == composite) {
return;
}
if (ignoreEmpty && nacosPropertySource.getSource().isEmpty()) {
return;
}
composite.addFirstPropertySource(nacosPropertySource);
}
@Override
public void setNacosConfigManager(NacosConfigManager nacosConfigManager) {
this.nacosConfigManager = nacosConfigManager;
}
}
public class OvseNacosPropertySourceBuilder extends NacosPropertySourceBuilder {
private static final Logger log = LoggerFactory
.getLogger(NacosPropertySourceBuilder.class);
private ConfigService configService;
private long timeout;
private OvseNacosCipherConfigProcessor ovseNacosCipherConfigProcessor;
public OvseNacosPropertySourceBuilder(ConfigService configService, long timeout, OvseNacosCipherConfigProcessor ovseNacosCipherConfigProcessor) {
super(configService, timeout);
this.configService = configService;
this.timeout = timeout;
this.ovseNacosCipherConfigProcessor = ovseNacosCipherConfigProcessor;
}
@Override
public long getTimeout() {
return timeout;
}
@Override
public void setTimeout(long timeout) {
this.timeout = timeout;
}
@Override
public ConfigService getConfigService() {
return configService;
}
@Override
public void setConfigService(ConfigService configService) {
this.configService = configService;
}
/**
* @param dataId Nacos dataId
* @param group Nacos group
*/
@Override
NacosPropertySource build(String dataId, String group, String fileExtension,
boolean isRefreshable) {
List<PropertySource<?>> propertySources = loadNacosData(dataId, group,
fileExtension);
NacosPropertySource nacosPropertySource = new NacosPropertySource(propertySources,
group, dataId, new Date(), isRefreshable);
NacosPropertySourceRepository.collectNacosPropertySource(nacosPropertySource);
return nacosPropertySource;
}
private List<PropertySource<?>> loadNacosData(String dataId, String group,
String fileExtension) {
String data = null;
try {
data = configService.getConfig(dataId, group, timeout);
if (StringUtils.isEmpty(data)) {
log.warn(
"Ignore the empty nacos configuration and get it based on dataId[{}] & group[{}]",
dataId, group);
return Collections.emptyList();
}
if (log.isDebugEnabled()) {
log.debug(String.format(
"Loading nacos data, dataId: '%s', group: '%s', data: %s", dataId,
group, data));
}
//ovse cipher config process
data = this.ovseNacosCipherConfigProcessor.process(data);
return NacosDataParserHandler.getInstance().parseNacosData(dataId, data,
fileExtension);
}
catch (NacosException e) {
log.error("get data from Nacos error,dataId:{} ", dataId, e);
}
catch (Exception e) {
log.error("parse data from Nacos error,dataId:{},data:{}", dataId, data, e);
}
return Collections.emptyList();
}
}
测试后发现这个方法实现了需求。
后续我像运维提供了密文配置的生成工具,完成了整套加密配置的处理。