公司有的项目里面还是采用把API key明文写在application.properties或者某个单独的properties文件来管理,这既不安全也不符合公司的规定。所以我通过利用观察者模式和Spring的事件机制,将其改为加密存储于配置中心,并且实现了在配置中心更新后,服务中生效的配置也可以立即更新。
背景
谈论实现之前,我先说一下我有哪些工具可以使用:
- SecretKeyService:一个可以保存对称及非对称加密密钥的服务
- CredentialVault:一个用来存放密钥的服务,密钥一旦存入,就只能再通过API的方式得到其原文
- ConfigurationCenter:配置中心,不提供任何加密功能,在配置内容更新后可以通过Spring的事件通知到使用了这个配置的服务
接到这个需求后,我下意识地觉得应该用CredentialVault解决问题,但是仔细想想,发现并不能。因为按照公司要求,API key的密码必须定期轮换,而老的密码在轮换之后就会马上失效。尽管我们有两套API key来避免前面的问题,但是更换API key密钥对又需要修改配置文件,并经历code review及上线部署,依旧需要一定的人工操作。我的设想是,只需要在一个地方设定好要生效的API key,接下来所有用到这个API key的服务都能自动更新。这样看来,似乎带有通知功能的配置中心是唯一解。但是配置中心不提供加密功能,所以还需要SecretKeyService提供一个对称加密密钥来把API key的密码加密,然后将它放到配置中心。
记不住这些名字没关系,后面你也看不到它们了。
实现
在确定要用的工具之后,就可以开始着手将它们拼装在一起了。因为原本公司的代码需要保密,所以下面的代码更多是展示思路,大概率你不能直接拷出来放到你的项目中用。
PropertyLoader
因为直接把配置源从文件换成配置中心有一定的风险,保险起见我们决定逐步迁移配置,在迁移期间需要同时支持配置文件和配置中心。所以,就诞生了PropertyLoader。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| public class PropertyLoader { private final EncryptionDecryptionService encryptionDecryptionService; private final ConfigurationCenterClient configurationCenterClient; private final Properties properties;
public PropertyLoader( final EncryptionDecryptionService encryptionDecryptionService, final ConfigurationCenterClient configurationCenterClient, final Properties properties, ) { this.encryptionDecryptionService = encryptionDecryptionService; this.configurationCenterClient = configurationCenterClient; this.properties = properties; }
public String loadProperty(final String key) { String value = configurationCenterClient.getValue(key); if (value == null) { value = properties.getProperty(key); } if (value == null) { throw new PropertyNotFoundException(key); }
return value; }
public String loadEncryptedProperty(final String key) { final String encryptedValue = configurationCenterClient.getValue(key); if (encryptedValue == null) { final String plainValue = properties.getProperty(key); if (plainValue == null) { throw new PropertyNotFoundException(key); }
return plainValue; }
return encryptionDecryptionService.decrypt(encryptedValue); }
public boolean hasProperty(final String key) { return configurationCenterClient.getKeys().contains(key) || properties.containsKey(key); } }
|
实现观察者模式
上面说到了,我需要在监听到配置中心发出的事件后,更新相关对象中的配置。显然,在每个对象中都实现一个Spring事件监听器是很蠢的,我们应该在一处监听Spring事件,然后将其以某种方式广播到相关的对象。看起来,观察者模式是个不错的选择。
观察者模式包含两个组件:notifier和subject。Subject作为观众,观察着某个事件;notifier则负责将事件通知给各个subject。
OK,理论有这些就够了。接下来我们把它实现。首先是subject。
观察者模式 - subject
首选我们用一个抽象类定义一个观察者要有的行为,需要成为观察者的类将会继承这个抽象类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| public abstract class ConfigRefreshObserverSubject { private final PropertyLoader propertyLoader; private final List<String> monitoredKeys; private String configSignature;
public ConfigRefreshObserverSubject( final PropertyLoader propertyLoader, final List<String> monitoredKeys ) { this.propertyLoader = propertyLoader; this.monitoredKeys = monitoredKeys; this.configSignature = calculateConfigurationSignature(); }
public abstract void refreshConfigImpl();
public final void refreshConfig() { refreshConfigImpl(); configSignature = calculateConfigurationSignature(); }
public PropertyLoader getPropertyLoader() { return this.propertyLoader; }
public String getConfigSignature() { return this.configSignature; }
protected String calculateConfigurationSignature() { final StringBuilder newConfigurationSignatureSeedBuilder = new StringBuilder(); for (String key : monitoredKeys) { if (!propertyLoader.hasProperty(key)) { throw new IllegalArgumentException("Missing property: " + key + " for class: " + this.getClass().getSimpleName()); }
newConfigurationSignatureSeedBuilder.append(propertyLoader.loadProperty(key)); }
return UUID.nameUUIDFromBytes(newConfigurationSignatureSeedBuilder.toString().getBytes()).toString(); } }
|
观察者模式 - notifier
在观察者模式中,notifier将作为一个单例存在,各个subject会注册到这个notifier,并在接收到事件后被notifier逐个通知。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| public class ConfigRefreshObserverNotifier { private static final Map<String, ConfigRefreshObserverSubject> subjects = new HashMap<>();
private ConfigRefreshObserverNotifier() { throw new UnsupportedOperationException("ConfigRefreshObserverNotifier shouldn't be instantiated"); }
public static void register(final ConfigRefreshObserverSubject subject) { if (!isSubjectRegistered(subject)) { subjects.put(subject.getClass().getSimpleName(), subject); } }
public static void notifyObservers() { for (final ConfigRefreshObserverSubject subject : subjects.values()) { final String newConfigSignature = subject.calculateConfigurationSignature(); if (!newConfigSignature.equals(subject.getConfigSignature())) { subject.refreshConfig(); } } }
private static boolean isSubjectRegistered(final ConfigRefreshObserverSubject subject) { final String subjectClassName = subject.getClass().getSimpleName(); return subjects.containsKey(subjectClassName); } }
|
观察者模式 - 实际的观察者类
有了subject,接下来就可以让实际要监控配置更新的类继承ConfigRefreshObserverSubject,将它变为一个观察者,并实现更新配置的逻辑。这部分其实很简单,就是给对应的字段重新赋值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public class SomeServiceClient extends ConfigRefreshObserverSubject { private String endpoint; private String username; private String password;
public SomeServiceClient(final PropertyLoader propertyLoader) { super( propertyLoader, List.of("some-service-endpoint", "some-service-username", "some-service-password") );
initializeProperties(); ConfigRefreshObserverNotifier.register(this); }
private void initializeProperties() { final PropertyLoader propertyLoader = super.getPropertyLoader();
this.endpoint = propertyLoader.loadProperty("some-service-endpoint"); this.username = propertyLoader.loadProperty("some-service-username"); this.password = propertyLoader.loadEncryptedProperty("some-service-password"); }
@Override public void refreshConfigImpl() { initializeProperties(); }
}
|
上面的代码应该很容易理解,在配置刷新前,SomeServiceClient就用当前生效的配置去发请求,在配置刷新后,这个观察者就可以马上得知这个事件并从配置中心取得最新的值替换当前生效的配置。这个过程几乎是瞬间完成的,不会对业务产生影响。
监听配置刷新事件
上面洋洋洒洒实现了一堆东西,但最重要的一个还没有实现,那就是配置刷新事件的监听器。因为配置中心会通过Spring事件来发布,所以只需要找个地方实现一个@EventListener方法就行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Configuration public class BeanFactory { @Bean public SomeServiceClient someServiceClient( final ResourceLoader resourceLoader, final EncryptionDecryptionService encryptionDecryptionService ) { final Properties properties = ResourcesUtils.loadProperties( resourceLoader, ResourcesUtils.CLASSPATH_META_INF + "/some-service.properties" );
final PropertyLoader propertyLoader = new PropertyLoader(encryptionDecryptionService, properties); return new SomeServiceClient(propertyLoader); }
@EventListener public void handleConfigRefreshEvent(final ConfigRefreshEvent event) { if (event.getProjects().contains("my-config-project")) { ConfigRefreshObserverNotifier.notifyObservers(); } } }
|
以上,就完成了一个利用配置中心的通知机制实现的配置动态更新功能。