【基于 Swoole+Hyperf 的微服务实战】 第四周·周四:多环境配置管理与配置加密
今天我们进入的主题是 多环境配置管理与配置加密。在实际微服务项目中,开发、测试、生产环境的配置各不相同,且敏感信息(如数据库密码、API 密钥)绝不能以明文形式暴露。昨天我们已经将配置迁移到了 Nacos,今天将在此基础上实现多环境隔离和配置加密,让应用在任意环境下都能安全、正确地运行,做到“一处打包,处处运行”。

今日目标
一、环境准备(约 20 分钟)
继续使用 hyperf-app 项目和 Docker 环境,确保 Nacos、Consul 等服务已启动。
docker-compose exec swoole bash
cd /var/www/hyperf-app
今天我们需要在 Nacos 中创建多个命名空间,并为每个环境准备不同的配置。同时为了演示加密,我们会生成一个 AES 密钥用于加解密。
二、知识核心:多环境策略与配置加密原理(约 1 小时)
1. 多环境配置管理方法
常见有三种模式:
- 多文件法:.env.dev、.env.prod,启动时通过环境变量指定。不利于集中管理。
- 配置中心分组:Nacos 提供 Namespace(命名空间),每个空间有独立的配置列表。不同环境使用不同 Namespace,服务通过 namespace_id 自动加载对应配置。
- Data ID 前缀法:同一个 Namespace 下通过 Data ID 携带环境后缀,如 hyperf-app-dev、hyperf-app-prod,然后根据 APP_ENV 动态拼接。
推荐使用 Namespace,因为它能物理隔离配置,还可以结合权限控制(生产环境配置仅运维可见)。
2. 配置加密的重要性
配置文件中的密码、Token、证书等,一旦泄露后果严重。配置加密要做到:
- 存储加密:提交到 Git 或配置中心的是密文,即使被看到也无法直接使用。
- 透明解密:应用启动时自动解密,业务代码不用修改。
- 密钥管理:解密密钥不能与密文存放在一起,可通过环境变量、密钥管理服务(如 KMS)或容器注入。
我们将演示一个轻量方案:自定义加密/解密函数,使用 AES-256-CBC 算法,密钥通过环境变量注入,绝不写入配置文件。
三、实战:多环境隔离与加密存储(约 2.5 小时)
步骤 1:在 Nacos 中创建 dev 和 prod 命名空间
- 命名空间名:dev
- 命名空间ID:输入自定义 ID(如 dev-001),或者自动生成,但我们需要记住它。
创建完成后,在命名空间列表能看到两个空间及各自的 ID。
重要:复制两个命名空间的 ID,后面配置要用。
步骤 2:在各命名空间中创建同名 Data ID
进入 配置列表,通过右上角切换到 dev 命名空间。新建配置:
- Data ID: hyperf-app (与昨天一致)
- 配置内容(dev 环境):
{
"app_name": "Hyperf-Dev",
"database": {
"default": {
"host": "mysql",
"port": 3306,
"database": "hyperf_blog_dev",
"username": "dev_user",
"password": "ENC(加密后的密码)"
}
},
"redis": {
"default": {
"host": "redis",
"password": ""
}
}
}
切换到 prod 命名空间,创建同名 Data ID hyperf-app,内容:
{
"app_name": "Hyperf-Prod",
"database": {
"default": {
"host": "mysql-prod.internal",
"port": 3306,
"database": "hyperf_blog_prod",
"username": "prod_user",
"password": "ENC(另一种加密)"
}
},
"redis": {
"default": {
"host": "redis.prod.internal",
"password": "ENC(redis密码密文)"
}
}
}
注意 password 字段我们使用了 ENC(…) 占位符,表示该值需要解密。
步骤 3:修改 config_center.php 支持动态命名空间
编辑 config/autoload/config_center.php,使 namespace_id 根据环境变量决定:
<?php
return [
'driver' => Hyperf\\ConfigNacos\\NacosDriver::class,
'client' => [
'host' => 'nacos',
'port' => 8848,
'username' => 'nacos',
'password' => 'nacos',
],
'config' => [
'data_id' => 'hyperf-app',
'group' => 'DEFAULT_GROUP',
'namespace_id' => env('NACOS_NAMESPACE_ID', ''), // 通过环境变量注入
'type' => 'json',
],
'listener' => [
'enable' => true,
'interval' => 3,
],
];
在 .env 中增加:
# 本地开发使用 dev 命名空间
NACOS_NAMESPACE_ID=dev-001
如果切换到生产环境,只需修改该值为 prod-001(实际部署时通过环境变量注入,不要写在 .env 中)。
步骤 4:实现配置解密服务
我们创建一个工具类 App\\Helpers\\ConfigDecryptor,负责在配置加载后扫描包含 ENC(…) 的值并解密。
生成 AES 密钥(示例,生产需安全生成):
php -r "echo base64_encode(random_bytes(32));" # 输出类似 6a7b… 的 Base64 字符串
将这个密钥通过环境变量注入(绝不能写在代码里),在 .env 中添加:
CONFIG_ENCRYPT_KEY=你的Base64密钥
创建 app/Helpers/ConfigDecryptor.php:
<?php
namespace App\\Helpers;
use Hyperf\\Contract\\ConfigInterface;
use Hyperf\\Di\\Annotation\\Inject;
use Hyperf\\Utils\\ApplicationContext;
class ConfigDecryptor
{
private string $key;
private string $cipher = 'aes-256-cbc';
public function __construct()
{
$this->key = base64_decode(env('CONFIG_ENCRYPT_KEY', ''));
if (strlen($this->key) !== 32) {
throw new \\RuntimeException('AES key must be 32 bytes (256 bits).');
}
}
/**
* 解密配置数组中所有以 ENC() 包裹的值
*/
public function decryptArray(array $config): array
{
array_walk_recursive($config, function (&$value) {
if (is_string($value) && str_starts_with($value, 'ENC(') && str_ends_with($value, ')')) {
$encrypted = substr($value, 4, –1);
$value = $this->decrypt($encrypted);
}
});
return $config;
}
public function encrypt(string $plaintext): string
{
$iv = random_bytes(openssl_cipher_iv_length($this->cipher));
$encrypted = openssl_encrypt($plaintext, $this->cipher, $this->key, OPENSSL_RAW_DATA, $iv);
return base64_encode($iv . $encrypted);
}
private function decrypt(string $payload): string
{
$data = base64_decode($payload);
$ivLength = openssl_cipher_iv_length($this->cipher);
$iv = substr($data, 0, $ivLength);
$encrypted = substr($data, $ivLength);
return openssl_decrypt($encrypted, $this->cipher, $this->key, OPENSSL_RAW_DATA, $iv);
}
}
步骤 5:在配置加载后触发解密
Hyperf 在配置中心拉取配置后会触发一个事件 Hyperf\\ConfigCenter\\Event\\ConfigChanged(或类似),我们可以监听该事件对配置进行后处理。更简单的方法是:在 Nacos 驱动加载配置后会合并到 ConfigInterface,我们可以通过一个 BootApplication 监听器来解密配置。
创建 app/Listener/DecryptConfigListener.php:
<?php
namespace App\\Listener;
use App\\Helpers\\ConfigDecryptor;
use Hyperf\\ConfigCenter\\Event\\ConfigChanged;
use Hyperf\\Contract\\ConfigInterface;
use Hyperf\\Event\\Annotation\\Listener;
use Hyperf\\Event\\Contract\\ListenerInterface;
use Hyperf\\Di\\Annotation\\Inject;
#[Listener]
class DecryptConfigListener implements ListenerInterface
{
#[Inject]
private ConfigInterface $config;
#[Inject]
private ConfigDecryptor $decryptor;
public function listen(): array
{
return [
ConfigChanged::class,
];
}
public function process(object $event)
{
// 获取所有配置,解密后重新设置
// 注意:$config->all() 可能不存在,这里假设可以获取所有配置
// 更实际的做法是只处理数据库、redis 等关键配置,避免全量
$dbConfig = $this->config->get('databases.default', []);
if (!empty($dbConfig)) {
$decrypted = $this->decryptor->decryptArray($dbConfig);
$this->config->set('databases.default', $decrypted);
}
$redisConfig = $this->config->get('redis.default', []);
if (!empty($redisConfig)) {
$decrypted = $this->decryptor->decryptArray($redisConfig);
$this->config->set('redis.default', $decrypted);
}
// 也可以处理其他配置项…
}
}
由于 ConfigChanged 事件可能需要配置中心组件触发,如果未触发,我们也可以在 BootApplication 事件中执行一次初始化解密。为了确保数据库密码在连接池创建前被解密,建议在 BeforeMainServerStart 或 OnWorkerStart 事件中处理。这里为了简单,我们直接在 config_center.php 的 merge_mode 配合自定义配置解析器?或者我们在 ConfigDecryptor 中采用更直接的方法:在 config/autoload/databases.php 中不写真实密码,而是从环境变量中读,然后结合解密逻辑。但这样违背了配置中心集中管理的理念。
一个更稳健的实战方案:只在 Nacos 中存储密文,在数据库配置文件中通过环境变量 DB_PASSWORD 传递明文,而 DB_PASSWORD 又由外部注入解密后的值。不过我们今天是教学环境,可以演示在应用层解密配置。
我们采用监听 BootApplication 的方式确保数据库配置在连接池启动前被解密。修改 DecryptConfigListener:
use Hyperf\\Framework\\Event\\BootApplication;
#[Listener]
class DecryptConfigListener implements ListenerInterface
{
public function listen(): array
{
return [
BootApplication::class,
];
}
public function process(object $event)
{
// 处理 databases 和 redis 配置
$dbKey = 'databases';
$redisKey = 'redis';
// …
}
}
但这样只能解密一次,后续 Nacos 更新不会自动解密。为了完美,需要监听配置中心的 OnPipeMessage 或 ConfigChanged 事件。在 Hyperf 3.0+ 中,配置中心驱动的拉取流程中会触发 ConfigChanged,我们可以复用。
步骤 6:加密实际密码并更新 Nacos
利用 ConfigDecryptor 的 encrypt 方法加密真实密码。编写一个简单的命令行脚本 bin/encrypt-config.php:
#!/usr/bin/env php
<?php
use App\\Helpers\\ConfigDecryptor;
require __DIR__.'/../vendor/autoload.php';
$decryptor = new ConfigDecryptor();
echo "加密 'dev_password': " . $decryptor->encrypt('dev_password') . PHP_EOL;
echo "加密 'prod_password': " . $decryptor->encrypt('prod_password') . PHP_EOL;
运行得到密文,将其填入 Nacos 配置的 password 字段,格式为 ENC(密文)。
步骤 7:验证多环境切换与解密
重启服务(确保 APP_ENV 对应 dev 且 NACOS_NAMESPACE_ID=dev-001):
php bin/hyperf.php start
访问任意需要数据库的接口,系统正常连接数据库(数据库密码是解密后的明文)。如果我们在 config/autoload/databases.php 中仍保留静态配置,需要注意优先级:Nacos 配置会覆盖本地配置,所以本地可以不写真实密码。
通过 config() 函数查看数据库密码是否已解密为明文(注意安全,仅测试用):
在控制器中临时添加:
return ['db_pass' => config('databases.default.password')];
应返回明文密码,证明解密成功。
现在修改 .env 中 NACOS_NAMESPACE_ID 为 prod-001,重启服务。如果 prod 命名空间中的数据库指向不同的服务器,则应用将连接到生产库。当然我们只是演示配置切换,实际不连生产库。
四、成果测试与验证(约 1 小时)
测试清单
| Nacos 多命名空间创建 | Nacos 控制台查看 | 存在 dev 和 prod 两个空间,各有配置 |
| 环境变量切换 | 修改 .env 中 NACOS_NAMESPACE_ID,重启服务 | 服务加载对应空间的配置(可通过测试接口查看 app_name) |
| 配置加密存储 | Nacos 控制台查看密码字段 | 显示为 ENC(密文) 形式,非明文 |
| 应用解密成功 | 访问测试接口查看数据库密码字段(或连接成功) | 密码为明文,数据库连接正常 |
| 密钥安全 | CONFIG_ENCRYPT_KEY 仅存在于 .env(不提交到 Git),代码中无硬编码 | 检查 Git 提交不包含密钥 |
| 配置更新解密 | 修改 Nacos 中加密字段的密文,等待服务刷新后再次请求 | 新密码生效(数据库连接可能需重连,但配置已更新) |
常见问题
- 解密失败:确保密钥长度 32 字节,Base64 编码正确。密文格式 ENC(…) 解析无误。
- 数据库连接失败:解密后密码可能仍不正确,检查加密时原始密码。
- 命名空间 ID 错误:确保 .env 中的 ID 与 Nacos 命名空间 ID 完全一致。
- 配置覆盖顺序:如果本地 databases.php 中的密码也配置了,Nacos 会覆盖,但解密监听器应该处理最终值。
五、今日作业与学习产出
- 加密所有敏感配置项(Redis 密码、JWT 密钥等),并测试解密。
- 研究 hyperf/encryption 组件或自定义注解实现更优雅的加密字段标记。
- 画出多环境配置管理的架构图:代码仓库(无敏感信息)→ CI/CD 注入环境变量 → 配置中心加载 + 解密 → 应用运行。
- 对比不同配置加密方案(环境变量、配置文件加密工具、KMS),分析各自的优缺点和适用场景。
- 使用 Docker Secrets 或 Kubernetes Secrets 传递解密密钥,而不是环境变量。
- 研究 HashiCorp Vault 等专业密钥管理工具,思考在企业级项目中如何与 Hyperf 集成。
通过今天的学习,你实现了微服务配置管理的“最后一公里”:环境隔离与安全加密。这标志着你的服务已经具备生产级部署的成熟度,可以安全地在多个环境中流转。明天我们将进行第二阶段综合实战,把服务注册、配置中心、熔断限流等全部串联,打造一个完整的微服务治理体系。


