您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

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

时间:2021-11-10 09:49:16  来源:  作者:古德的程序员

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

背景

最近在忙脚手架升级,为了减少中间件依赖,降低学习成本,将原来使用的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扩展点对数据做加解密处理,我尝试了两个方式

  • EnvironmentPostProcessor
  • PropertySourceLoader

但是经过试验,两个扩展点的切入都是在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();
    }
}

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

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



Tags:Nacos   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
原创文章,转载请注明出处背景最近在忙脚手架升级,为了减少中间件依赖,降低学习成本,将原来使用的Apollo弃用,和服务发现一同使用 Nacos 来实现。后面公司安全部门做安全检查,要求...【详细内容】
2021-11-10  Tags: Nacos  点击:(74)  评论:(0)  加入收藏
本文作者:HelloGitHub-秦人HelloGitHub 推出的《讲解开源项目》系列,今天给大家带来一款开源 Java 版可以实现动态服务发现,配置和服务管理平台&mdash;&mdash;Nacos,它是阿里巴...【详细内容】
2020-11-12  Tags: Nacos  点击:(109)  评论:(0)  加入收藏
▌简易百科推荐
摘 要 (OF作品展示)OF之前介绍了用python实现数据可视化、数据分析及一些小项目,但基本都是后端的知识。想要做一个好看的可视化大屏,我们还要学一些前端的知识(vue),网上有很多比...【详细内容】
2021-12-27  项目与数据管理    Tags:Vue   点击:(1)  评论:(0)  加入收藏
程序是如何被执行的&emsp;&emsp;程序是如何被执行的?许多开发者可能也没法回答这个问题,大多数人更注重的是如何编写程序,却不会太注意编写好的程序是如何被运行,这并不是一个好...【详细内容】
2021-12-23  IT学习日记    Tags:程序   点击:(9)  评论:(0)  加入收藏
阅读收获✔️1. 了解单点登录实现原理✔️2. 掌握快速使用xxl-sso接入单点登录功能一、早期的多系统登录解决方案 单系统登录解决方案的核心是cookie,cookie携带会话id在浏览器...【详细内容】
2021-12-23  程序yuan    Tags:单点登录(   点击:(8)  评论:(0)  加入收藏
下载Eclipse RCP IDE如果你电脑上还没有安装Eclipse,那么请到这里下载对应版本的软件进行安装。具体的安装步骤就不在这赘述了。创建第一个标准Eclipse RCP应用(总共分为六步)1...【详细内容】
2021-12-22  阿福ChrisYuan    Tags:RCP应用   点击:(7)  评论:(0)  加入收藏
今天想简单聊一聊 Token 的 Value Capture,就是币的价值问题。首先说明啊,这个话题包含的内容非常之光,Token 的经济学设计也可以包含诸多问题,所以几乎不可能把这个问题说的清...【详细内容】
2021-12-21  唐少华TSH    Tags:Token   点击:(9)  评论:(0)  加入收藏
实现效果:假如有10条数据,分组展示,默认在当前页面展示4个,点击换一批,从第5个开始继续展示,到最后一组,再重新返回到第一组 data() { return { qList: [], //处理后...【详细内容】
2021-12-17  Mason程    Tags:VUE   点击:(14)  评论:(0)  加入收藏
什么是性能调优?(what) 为什么需要性能调优?(why) 什么时候需要性能调优?(when) 什么地方需要性能调优?(where) 什么时候来进行性能调优?(who) 怎么样进行性能调优?(How) 硬件配...【详细内容】
2021-12-16  软件测试小p    Tags:性能调优   点击:(19)  评论:(0)  加入收藏
Tasker 是一款适用于 Android 设备的高级自动化应用,它可以通过脚本让重复性的操作自动运行,提高效率。 不知道从哪里听说的抖音 app 会导致 OLED 屏幕烧屏。于是就现学现卖,自...【详细内容】
2021-12-15  ITBang    Tags:抖音防烧屏   点击:(23)  评论:(0)  加入收藏
11 月 23 日,Rust Moderation Team(审核团队)在 GitHub 上发布了辞职公告,即刻生效。根据公告,审核团队集体辞职是为了抗议 Rust 核心团队(Core team)在执行社区行为准则和标准上...【详细内容】
2021-12-15  InfoQ    Tags:Rust   点击:(24)  评论:(0)  加入收藏
一个项目的大部分API,测试用例在参数和参数值等信息会有很多相似的地方。我们可以复制API,复制用例来快速生成,然后做细微调整既可以满足我们的测试需求1.复制API:在菜单发布单...【详细内容】
2021-12-14  AutoMeter    Tags:AutoMeter   点击:(20)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条