Spring Boot中的profile概念使得提供针对运行环境的配置变得非常容易。一个典型的设置可能包括本地开发环境、测试环境、暂存和生产环境的配置。你所需要做的就是在classpath上为每个环境提供一个属性文件,例如application-dev.properties 和application-prod.properties 。在运行时,你可以通过命令行选项或环境变量告诉Spring Boot选择哪个配置文件。
现在,不同的环境需要不同的凭证。比方说,你的应用程序在开发时使用嵌入式H2数据库,在所有其他环境中使用PostgreSQL。感觉很自然,只需在配置文件的属性文件中添加用户名/密码或访问令牌。在构建Spring Boot应用程序的二进制文件、可执行JAR或WAR文件时,这些文件将简单地包含在可交付的文件中。
根据你计划分享二进制文件的方式和应用的部署策略,这种方法可能会引起安全问题。是否应该允许任何能够访问该文件的人破解存档并访问纯文本凭证?如果你为你的应用程序建立一个Docker镜像,并在Docker注册表上分享它呢?你可能已经猜到了。这可能是一个更好的主意,将凭证外部化并在运行时注入,以消除潜在的安全漏洞。
在这篇博文中,我将向你展示如何使用Docker secrets来在Docker Swarm中创建凭证。我们将在该Swarm上运行一个分布在多个副本中的Spring Boot应用,并在为该应用创建服务时才注入凭证。
Docker secrets是一项企业功能。不幸的是,如果不在Swarm中分发你的应用程序,你就无法使用Docker secrets。在未来的一篇文章中,我将讨论存储证书的其他选择,如HashiCorp的Vault。
读取Docker secrets文件
在Spring Boot应用程序的不同环境之间进行切换很容易,即使它是在Docker容器中运行的。只需在启动容器时传递环境变量SPRING_PROFILES_ACTIVE ,并将其指向所需的配置文件名称。此外,如果Docker secrets文件确实存在的话,我们要从该文件中读取凭证。默认情况下,当在Docker Swarm的一个节点上运行时,Docker secrets在目录/run/secrets 。
读取Docker机密文件的一个很好的方法是实现一个EnvironmentPostProcessor。假设我们想读取一个数据库密码并将其用于我们应用程序的数据源。列表1中所示的类正好实现了这一点。
DockerSecretsDatabasePasswordProcessor.java
package com.bmuschko.todo.webservice.env;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.util.StreamUtils;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Properties;
public class DockerSecretsDatabasePasswordProcessor implements EnvironmentPostProcessor {
private final Logger logger = LoggerFactory.getLogger(DockerSecretsDatabasePasswordProcessor.class);
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
Resource resource = new FileSystemResource("/run/secrets/db-password");
if (resource.exists()) {
try {
if (logger.isInfoEnabled()) {
logger.info("Using database password from injected Docker secret file");
}
String dbPassword = StreamUtils.copyToString(resource.getInputStream(), Charset.defaultCharset());
Properties props = new Properties();
props.put("spring.datasource.password", dbPassword);
environment.getPropertySources().addLast(new PropertiesPropertySource("dbProps", props));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
清单1.从Docker secrets文件中读取和应用数据库凭证
实现EnvironmentPostProcessor 是不够的。你必须在一个名为spring.factories 的文件中注册该类。对于Maven和Gradle项目,该文件通常位于src/main/resources ,在创建可执行二进制文件时与应用程序捆绑在一起。
META-INF/spring.plants
org.springframework.boot.env.EnvironmentPostProcessor=com.bmuschko.todo.webservice.env.DockerSecretsDatabasePasswordProcessor
该应用程序已经准备就绪。接下来,我们来看看容器的基础设施。
创建一个Docker秘密
使用Docker秘密的前提条件是有一个Docker Swarm。一个Docker Swarm必须至少有一个管理节点。此外,你可以有任何数量的工作节点。如果你想了解更多关于容错和负载平衡的信息,请参考Docker文档。
你可以在Swarm管理器上用一条命令创建一个Docker秘密。下面的命令创建了包含密码prodpwd 的秘密db-password 。
$ printf "prodpwd" | docker secret create db-password -
创建秘密后,你应该可以列出它。
$ docker secret ls
ID NAME DRIVER CREATED UPDATED
nskgie2gozso364hwl92ne3og db-password 12 days ago 12 days ago
我们创建了一个秘密。让我们把它注入运行我们应用程序的容器中。
使用Docker秘密
在Docker中,你可以在服务的帮助下将一个应用程序分布在多个节点上。创建服务时,你可以提供设置环境变量和Docker秘密的选项。
假设我们想在生产环境中运行一个Spring Boot应用程序。首先,我们用--env SPRING_PROFILES_ACTIVE=prod 激活配置文件。其次,我们通过--secret db-password 传入带有名称的Docker秘密。下面你可以看到创建一个有五个副本的Docker服务的完整命令。
$ docker service create --name todo-web-service --publish 8080:8080 --replicas 5 --secret db-password --env SPRING_PROFILES_ACTIVE=prod bmuschko/todo-web-service:latest
一切就绪。你的Spring Boot应用程序应该能够读取Docker秘密并使用它来连接到数据库。
总结
无论你是在容器中还是作为一个普通的可执行JAR文件运行,都不应该把凭证放在Spring Boot应用的二进制文件中。Docker secrets可以帮助保持凭证的安全性。在Docker Swarm环境中,很容易将秘密注入正在运行的容器中。从Spring Boot应用中读取秘密只需要实现一个后置处理器。