【基于 Swoole+Hyperf 的微服务实战】 第四周·周四:多环境配置管理与配置加密
今天我们进入的主题是多环境配置管理与配置加密。在实际微服务项目中,开发、测试、生产环境的配置各不相同,且敏感信息(如数据库密码、API 密钥)绝不能以明文形式暴露。昨天我们已经将配置迁移到了 Nacos,今天将在此基础上实现多环境隔离和配置加密,让应用在任意环境下都能安全、正确地运行,做到“一处打包,处处运行”。
今日目标
- 理解多环境配置的常见模式,使用 Nacos命名空间实现 dev/test/prod 环境隔离。
- 掌握让 Hyperf 根据环境变量动态选择 Nacos 配置集的技巧。
- 学习配置加密的核心思路,使用AES 加密保护数据库密码等敏感项。
- 编写一个配置解密服务,在应用启动时自动解密加密字段,使应用无感使用。
- 验证多环境切换效果:通过修改
APP_ENV即可加载不同 Nacos 配置,敏感信息全程密文存储。
一、环境准备(约 20 分钟)
继续使用hyperf-app项目和 Docker 环境,确保 Nacos、Consul 等服务已启动。
docker-composeexecswoolebashcd/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 命名空间
- 打开 Nacos 控制台(
http://localhost:8848/nacos),进入左侧菜单命名空间。 - 点击
新建命名空间:- 命名空间名:
dev - 命名空间ID:输入自定义 ID(如
dev-001),或者自动生成,但我们需要记住它。
- 命名空间名:
- 再次新建
prod命名空间(ID 自定,如prod-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 IDhyperf-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根据环境变量决定:
<?phpreturn['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:
<?phpnamespaceApp\Helpers;useHyperf\Contract\ConfigInterface;useHyperf\Di\Annotation\Inject;useHyperf\Utils\ApplicationContext;classConfigDecryptor{privatestring$key;privatestring$cipher='aes-256-cbc';publicfunction__construct(){$this->key=base64_decode(env('CONFIG_ENCRYPT_KEY',''));if(strlen($this->key)!==32){thrownew\RuntimeException('AES key must be 32 bytes (256 bits).');}}/** * 解密配置数组中所有以 ENC() 包裹的值 */publicfunctiondecryptArray(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;}publicfunctionencrypt(string$plaintext):string{$iv=random_bytes(openssl_cipher_iv_length($this->cipher));$encrypted=openssl_encrypt($plaintext,$this->cipher,$this->key,OPENSSL_RAW_DATA,$iv);returnbase64_encode($iv.$encrypted);}privatefunctiondecrypt(string$payload):string{$data=base64_decode($payload);$ivLength=openssl_cipher_iv_length($this->cipher);$iv=substr($data,0,$ivLength);$encrypted=substr($data,$ivLength);returnopenssl_decrypt($encrypted,$this->cipher,$this->key,OPENSSL_RAW_DATA,$iv);}}步骤 5:在配置加载后触发解密
Hyperf 在配置中心拉取配置后会触发一个事件Hyperf\ConfigCenter\Event\ConfigChanged(或类似),我们可以监听该事件对配置进行后处理。更简单的方法是:在 Nacos 驱动加载配置后会合并到ConfigInterface,我们可以通过一个BootApplication监听器来解密配置。
创建app/Listener/DecryptConfigListener.php:
<?phpnamespaceApp\Listener;useApp\Helpers\ConfigDecryptor;useHyperf\ConfigCenter\Event\ConfigChanged;useHyperf\Contract\ConfigInterface;useHyperf\Event\Annotation\Listener;useHyperf\Event\Contract\ListenerInterface;useHyperf\Di\Annotation\Inject;#[Listener]classDecryptConfigListenerimplementsListenerInterface{#[Inject]privateConfigInterface$config;#[Inject]privateConfigDecryptor$decryptor;publicfunctionlisten():array{return[ConfigChanged::class,];}publicfunctionprocess(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:
useHyperf\Framework\Event\BootApplication;#[Listener]classDecryptConfigListenerimplementsListenerInterface{publicfunctionlisten():array{return[BootApplication::class,];}publicfunctionprocess(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<?phpuseApp\Helpers\ConfigDecryptor;require__DIR__.'/../vendor/autoload.php';$decryptor=newConfigDecryptor();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 会覆盖,但解密监听器应该处理最终值。
五、今日作业与学习产出
- 提交代码:将
ConfigDecryptor、DecryptConfigListener、修改后的config_center.php和.env.example(不含真实密钥)提交到 Git。 - 扩展实践:
- 加密所有敏感配置项(Redis 密码、JWT 密钥等),并测试解密。
- 研究
hyperf/encryption组件或自定义注解实现更优雅的加密字段标记。
- 学习笔记:
- 画出多环境配置管理的架构图:代码仓库(无敏感信息)→ CI/CD 注入环境变量 → 配置中心加载 + 解密 → 应用运行。
- 对比不同配置加密方案(环境变量、配置文件加密工具、KMS),分析各自的优缺点和适用场景。
- 挑战任务:
- 使用Docker Secrets或Kubernetes Secrets传递解密密钥,而不是环境变量。
- 研究 HashiCorp Vault 等专业密钥管理工具,思考在企业级项目中如何与 Hyperf 集成。
通过今天的学习,你实现了微服务配置管理的“最后一公里”:环境隔离与安全加密。这标志着你的服务已经具备生产级部署的成熟度,可以安全地在多个环境中流转。明天我们将进行第二阶段综合实战,把服务注册、配置中心、熔断限流等全部串联,打造一个完整的微服务治理体系。