<返回更多

Nacos Config 支持配置加密解决方案

2021-11-10    古德的程序员
加入收藏

原创文章,转载请注明出处

背景

最近在忙脚手架升级,为了减少中间件依赖,降低学习成本,将原来使用的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官方文档,配置加密功能当前未支持,所以只好自己码。

Nacos Config 支持配置加密解决方案

 

我们的目标如下

初期尝试

最开始的尝试是希望依赖于Spring扩展点对数据做加解密处理,我尝试了两个方式

但是经过试验,两个扩展点的切入都是在Nacos将配置加载入Context之前,所以并不适用这次的需求。

也考虑到后期使用Nacos配置热更新的能力,放弃了直接从下层Spring扩展。

starter扩展

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();
    }
}

测试后发现这个方法实现了需求。

后续我像运维提供了密文配置的生成工具,完成了整套加密配置的处理。

声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多资讯 >>>