第 10 篇:配置管理 —— 别把密码写在代码里
在前面的文章中,我们探讨了可观测性等重要话题。现在,让我们关注另一个基础但至关重要的横切关注点:配置管理。
配置管理是保障系统安全和灵活性的重要手段。不正确的配置管理可能导致敏感信息泄露、环境混乱、部署困难等问题。
多环境配置(dev/staging/prod)
不同环境需要不同的配置,我们需要建立清晰的环境管理机制:
typescript
// 配置接口定义
export interface AppConfig {
// 环境配置
environment: 'development' | 'staging' | 'production';
serviceName: string;
version: string;
// 数据库配置
database: {
host: string;
port: number;
username: string;
password: string;
database: string;
ssl: boolean;
};
// 缓存配置
cache: {
host: string;
port: number;
password?: string;
ttl: number;
};
// JWT 配置
jwt: {
secret: string;
expiresIn: string;
refreshSecret: string;
refreshExpiresIn: string;
};
// 第三方服务配置
services: {
payment: {
apiKey: string;
apiUrl: string;
};
email: {
apiKey: string;
fromAddress: string;
};
};
// 监控配置
monitoring: {
sentryDsn?: string;
jaegerAgentHost?: string;
jaegerAgentPort?: number;
};
}
// 环境配置加载器
@Injectable()
export class ConfigService {
private config: AppConfig;
constructor() {
this.loadConfig();
}
private loadConfig(): void {
const environment = process.env.NODE_ENV || 'development';
// 基础配置
const baseConfig: Partial<AppConfig> = {
environment,
serviceName: process.env.SERVICE_NAME || 'saas-app',
version: process.env.VERSION || '1.0.0',
};
// 根据环境加载不同配置
let envConfig: Partial<AppConfig> = {};
switch (environment) {
case 'development':
envConfig = this.loadDevelopmentConfig();
break;
case 'staging':
envConfig = this.loadStagingConfig();
break;
case 'production':
envConfig = this.loadProductionConfig();
break;
default:
throw new Error(`Unknown environment: ${environment}`);
}
// 合并配置
this.config = {
...baseConfig,
...envConfig,
// 从环境变量覆盖特定配置
...this.loadFromEnvironmentVariables(),
} as AppConfig;
}
private loadDevelopmentConfig(): Partial<AppConfig> {
return {
database: {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432', 10),
username: process.env.DB_USERNAME || 'devuser',
password: process.env.DB_PASSWORD || 'devpass',
database: process.env.DB_NAME || 'saas_dev',
ssl: false,
},
cache: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379', 10),
ttl: 300,
},
jwt: {
secret: process.env.JWT_SECRET || 'dev-jwt-secret',
expiresIn: process.env.JWT_EXPIRES_IN || '15m',
refreshSecret: process.env.JWT_REFRESH_SECRET || 'dev-refresh-secret',
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
},
};
}
private loadStagingConfig(): Partial<AppConfig> {
return {
database: {
host: process.env.DB_HOST || 'staging-db.example.com',
port: parseInt(process.env.DB_PORT || '5432', 10),
username: process.env.DB_USERNAME || 'staginguser',
password: process.env.DB_PASSWORD || 'stagingpass', // 应该从密钥管理服务获取
database: process.env.DB_NAME || 'saas_staging',
ssl: true,
},
cache: {
host: process.env.REDIS_HOST || 'staging-redis.example.com',
port: parseInt(process.env.REDIS_PORT || '6379', 10),
ttl: 300,
},
jwt: {
secret: process.env.JWT_SECRET || 'staging-jwt-secret', // 应该从密钥管理服务获取
expiresIn: process.env.JWT_EXPIRES_IN || '15m',
refreshSecret: process.env.JWT_REFRESH_SECRET || 'staging-refresh-secret', // 应该从密钥管理服务获取
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
},
};
}
private loadProductionConfig(): Partial<AppConfig> {
return {
database: {
host: process.env.DB_HOST || 'prod-db.example.com',
port: parseInt(process.env.DB_PORT || '5432', 10),
username: process.env.DB_USERNAME || 'produser',
password: process.env.DB_PASSWORD || '', // 必须从密钥管理服务获取
database: process.env.DB_NAME || 'saas_prod',
ssl: true,
},
cache: {
host: process.env.REDIS_HOST || 'prod-redis.example.com',
port: parseInt(process.env.REDIS_PORT || '6379', 10),
password: process.env.REDIS_PASSWORD || '', // 应该从密钥管理服务获取
ttl: 300,
},
jwt: {
secret: process.env.JWT_SECRET || '', // 必须从密钥管理服务获取
expiresIn: process.env.JWT_EXPIRES_IN || '15m',
refreshSecret: process.env.JWT_REFRESH_SECRET || '', // 必须从密钥管理服务获取
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
},
};
}
private loadFromEnvironmentVariables(): Partial<AppConfig> {
const config: Partial<AppConfig> = {};
// 优先使用环境变量覆盖配置
if (process.env.SERVICE_NAME) {
config.serviceName = process.env.SERVICE_NAME;
}
if (process.env.VERSION) {
config.version = process.env.VERSION;
}
// 数据库配置
if (process.env.DB_HOST) {
config.database = {
...this.config.database,
host: process.env.DB_HOST,
};
}
return config;
}
get<T extends keyof AppConfig>(key: T): AppConfig[T] {
return this.config[key];
}
getDatabaseConfig(): AppConfig['database'] {
return this.config.database;
}
getCacheConfig(): AppConfig['cache'] {
return this.config.cache;
}
getJwtConfig(): AppConfig['jwt'] {
return this.config.jwt;
}
isDevelopment(): boolean {
return this.config.environment === 'development';
}
isStaging(): boolean {
return this.config.environment === 'staging';
}
isProduction(): boolean {
return this.config.environment === 'production';
}
}敏感信息加密(Vault / KMS / 配置中心)
敏感信息必须加密存储,不能明文出现在配置文件中:
typescript
// 密钥管理服务抽象
export interface KeyManagementService {
encrypt(plaintext: string): Promise<string>;
decrypt(ciphertext: string): Promise<string>;
getSecret(key: string): Promise<string>;
}
// Vault 实现
@Injectable()
export class VaultService implements KeyManagementService {
private vault: any; // Vault 客户端
constructor(private readonly configService: ConfigService) {
this.initializeVault();
}
private initializeVault(): void {
const vaultConfig = {
apiVersion: 'v1',
endpoint: process.env.VAULT_ADDR || 'http://127.0.0.1:8200',
token: process.env.VAULT_TOKEN,
};
// this.vault = require('node-vault')(vaultConfig);
}
async encrypt(plaintext: string): Promise<string> {
// Vault 通常不直接提供加密功能,而是管理密钥
throw new Error('Vault does not provide direct encryption');
}
async decrypt(ciphertext: string): Promise<string> {
throw new Error('Vault does not provide direct decryption');
}
async getSecret(key: string): Promise<string> {
try {
// 从 Vault 获取密钥
// const result = await this.vault.read(`secret/data/${key}`);
// return result.data.data.value;
return ''; // 占位符
} catch (error) {
throw new Error(`Failed to get secret ${key}: ${error.message}`);
}
}
}
// AWS KMS 实现
@Injectable()
export class KmsService implements KeyManagementService {
private kms: AWS.KMS;
constructor() {
this.kms = new AWS.KMS({
region: process.env.AWS_REGION || 'us-east-1',
});
}
async encrypt(plaintext: string): Promise<string> {
try {
const params = {
KeyId: process.env.KMS_KEY_ID,
Plaintext: Buffer.from(plaintext),
};
const result = await this.kms.encrypt(params).promise();
return result.CiphertextBlob.toString('base64');
} catch (error) {
throw new Error(`Failed to encrypt data: ${error.message}`);
}
}
async decrypt(ciphertext: string): Promise<string> {
try {
const params = {
CiphertextBlob: Buffer.from(ciphertext, 'base64'),
};
const result = await this.kms.decrypt(params).promise();
return result.Plaintext.toString();
} catch (error) {
throw new Error(`Failed to decrypt data: ${error.message}`);
}
}
async getSecret(key: string): Promise<string> {
// KMS 主要用于加密解密,密钥存储可能需要结合其他服务
throw new Error('KMS is not a secret storage service');
}
}
// 配置加密装饰器
export function Encrypted(): PropertyDecorator {
return function (target: Object, propertyKey: string | symbol) {
// 标记属性为加密字段
Reflect.defineMetadata('encrypted', true, target, propertyKey);
};
}
// 安全配置服务
@Injectable()
export class SecureConfigService {
constructor(
private readonly configService: ConfigService,
@Inject('KeyManagementService')
private readonly kmsService: KeyManagementService,
) {}
async getSecureConfig(): Promise<AppConfig> {
const baseConfig = this.configService as any;
const secureConfig = { ...baseConfig };
// 解密敏感字段
if (secureConfig.database?.password) {
secureConfig.database.password = await this.decryptIfEncrypted(
secureConfig.database.password
);
}
if (secureConfig.cache?.password) {
secureConfig.cache.password = await this.decryptIfEncrypted(
secureConfig.cache.password
);
}
if (secureConfig.jwt?.secret) {
secureConfig.jwt.secret = await this.decryptIfEncrypted(
secureConfig.jwt.secret
);
}
if (secureConfig.jwt?.refreshSecret) {
secureConfig.jwt.refreshSecret = await this.decryptIfEncrypted(
secureConfig.jwt.refreshSecret
);
}
return secureConfig;
}
private async decryptIfEncrypted(value: string): Promise<string> {
// 简单的加密标识检查
if (value.startsWith('enc:')) {
const encryptedValue = value.substring(4);
return await this.kmsService.decrypt(encryptedValue);
}
return value;
}
}动态配置热更新(无需重启)
配置热更新允许在不重启服务的情况下更新配置:
typescript
// 配置监听器
export interface ConfigListener {
onConfigChange(key: string, newValue: any, oldValue: any): void;
}
// 配置变更事件
export class ConfigChangeEvent {
constructor(
public readonly key: string,
public readonly newValue: any,
public readonly oldValue: any,
) {}
}
// 动态配置服务
@Injectable()
export class DynamicConfigService {
private config: Map<string, any> = new Map();
private listeners: Map<string, ConfigListener[]> = new Map();
private refreshInterval: NodeJS.Timeout;
constructor(
private readonly configService: ConfigService,
) {
this.initializeConfig();
this.startRefresh();
}
private initializeConfig(): void {
// 从静态配置初始化
const staticConfig = this.configService as any;
for (const [key, value] of Object.entries(staticConfig)) {
this.config.set(key, value);
}
}
private startRefresh(): void {
// 定期检查配置更新
this.refreshInterval = setInterval(() => {
this.refreshConfig();
}, 30000); // 每30秒检查一次
}
private async refreshConfig(): Promise<void> {
try {
// 这里可以实现从配置中心获取最新配置的逻辑
// 例如从 Consul、Etcd 或数据库获取
// 模拟配置更新
// const newConfig = await this.fetchConfigFromCenter();
// this.updateConfig(newConfig);
} catch (error) {
console.error('Failed to refresh config:', error);
}
}
get<T>(key: string): T {
return this.config.get(key);
}
set<T>(key: string, value: T): void {
const oldValue = this.config.get(key);
this.config.set(key, value);
// 通知监听器
this.notifyListeners(key, value, oldValue);
}
subscribe(key: string, listener: ConfigListener): void {
if (!this.listeners.has(key)) {
this.listeners.set(key, []);
}
this.listeners.get(key).push(listener);
}
unsubscribe(key: string, listener: ConfigListener): void {
const listeners = this.listeners.get(key);
if (listeners) {
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
}
}
}
private notifyListeners(key: string, newValue: any, oldValue: any): void {
const listeners = this.listeners.get(key);
if (listeners) {
const event = new ConfigChangeEvent(key, newValue, oldValue);
listeners.forEach(listener => {
try {
listener.onConfigChange(key, newValue, oldValue);
} catch (error) {
console.error('Config listener error:', error);
}
});
}
}
onModuleDestroy() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
}
}
// 配置控制器(用于动态更新配置)
@Controller('config')
@UseGuards(JwtAuthGuard)
export class ConfigController {
constructor(
private readonly dynamicConfigService: DynamicConfigService,
) {}
@Get(':key')
getConfigValue(@Param('key') key: string) {
return {
key,
value: this.dynamicConfigService.get(key),
};
}
@Put(':key')
updateConfigValue(
@Param('key') key: string,
@Body('value') value: any,
) {
this.dynamicConfigService.set(key, value);
return {
message: 'Config updated successfully',
key,
value,
};
}
}
// 配置感知的服务示例
@Injectable()
export class ConfigAwareService implements ConfigListener {
private cacheTtl: number;
constructor(
private readonly dynamicConfigService: DynamicConfigService,
) {
// 订阅配置变更
this.dynamicConfigService.subscribe('cache.ttl', this);
// 初始化配置值
this.cacheTtl = this.dynamicConfigService.get('cache.ttl') || 300;
}
onConfigChange(key: string, newValue: any, oldValue: any): void {
if (key === 'cache.ttl') {
this.cacheTtl = newValue;
console.log(`Cache TTL updated from ${oldValue} to ${newValue}`);
}
}
getCacheTtl(): number {
return this.cacheTtl;
}
}.env 文件的正确使用姿势
.env 文件是开发环境中常用的配置方式,但需要正确使用:
typescript
// .env 文件加载和验证
@Injectable()
export class EnvConfigService {
private readonly envConfig: Record<string, string>;
constructor() {
// 加载 .env 文件
dotenv.config();
// 验证必需的环境变量
this.validateRequiredVariables();
// 处理敏感变量
this.envConfig = this.processEnvironmentVariables();
}
private validateRequiredVariables(): void {
const requiredVariables = [
'NODE_ENV',
'DB_HOST',
'DB_PORT',
'DB_USERNAME',
'DB_NAME',
];
const missingVariables = requiredVariables.filter(
variable => !process.env[variable]
);
if (missingVariables.length > 0) {
throw new Error(
`Missing required environment variables: ${missingVariables.join(', ')}`
);
}
}
private processEnvironmentVariables(): Record<string, string> {
const config: Record<string, string> = {};
// 处理布尔值
for (const [key, value] of Object.entries(process.env)) {
if (value === 'true' || value === 'false') {
config[key] = value === 'true' ? 'true' : 'false';
} else if (/^\d+$/.test(value)) {
// 处理数字
config[key] = value;
} else {
config[key] = value;
}
}
return config;
}
get(key: string): string;
get(key: string, defaultValue: string): string;
get(key: string, defaultValue?: string): string {
const value = this.envConfig[key] || process.env[key];
if (value === undefined) {
if (defaultValue !== undefined) {
return defaultValue;
}
throw new Error(`Environment variable ${key} is not defined`);
}
return value;
}
getNumber(key: string): number;
getNumber(key: string, defaultValue: number): number;
getNumber(key: string, defaultValue?: number): number {
const value = this.get(key, defaultValue?.toString());
const numValue = Number(value);
if (isNaN(numValue)) {
throw new Error(`Environment variable ${key} is not a valid number`);
}
return numValue;
}
getBoolean(key: string): boolean;
getBoolean(key: string, defaultValue: boolean): boolean;
getBoolean(key: string, defaultValue?: boolean): boolean {
const value = this.get(key, defaultValue?.toString());
return value === 'true';
}
}
// 环境变量验证装饰器
export function Required(): PropertyDecorator {
return function (target: Object, propertyKey: string | symbol) {
Reflect.defineMetadata('required', true, target, propertyKey);
};
}
export function DefaultValue(value: any): PropertyDecorator {
return function (target: Object, propertyKey: string | symbol) {
Reflect.defineMetadata('defaultValue', value, target, propertyKey);
};
}
// 配置验证类
export class AppConfigValidator {
@Required()
@DefaultValue('development')
NODE_ENV: string;
@Required()
DB_HOST: string;
@Required()
@DefaultValue('5432')
DB_PORT: number;
@Required()
DB_USERNAME: string;
DB_PASSWORD: string; // 可选,但生产环境必需
@Required()
DB_NAME: string;
@DefaultValue('false')
DB_SSL: boolean;
static validate(): AppConfigValidator {
const config = new AppConfigValidator();
const errors: string[] = [];
// 验证必需字段
for (const [key, value] of Object.entries(config)) {
const required = Reflect.getMetadata('required', config, key);
const defaultValue = Reflect.getMetadata('defaultValue', config, key);
if (required && process.env[key] === undefined) {
if (defaultValue !== undefined) {
config[key] = defaultValue;
} else {
errors.push(`Required environment variable ${key} is missing`);
}
} else if (process.env[key] !== undefined) {
// 类型转换
if (typeof value === 'number') {
config[key] = Number(process.env[key]);
} else if (typeof value === 'boolean') {
config[key] = process.env[key] === 'true';
} else {
config[key] = process.env[key];
}
}
}
if (errors.length > 0) {
throw new Error(`Configuration validation failed:\n${errors.join('\n')}`);
}
// 生产环境特殊验证
if (config.NODE_ENV === 'production') {
if (!config.DB_PASSWORD) {
throw new Error('DB_PASSWORD is required in production environment');
}
}
return config;
}
}创业团队行动清单
立即行动:
- 建立多环境配置管理机制
- 将敏感信息从代码中移除
- 实现基本的配置验证
一周内完成:
- 集成密钥管理服务(如 Vault 或 AWS KMS)
- 实现配置热更新机制
- 添加配置变更监控
一月内完善:
- 建立完整的配置中心
- 实现配置版本管理和回滚
- 添加配置审计日志
总结
配置管理是保障系统安全和灵活性的重要手段,正确的实现需要:
- 环境隔离:建立清晰的多环境配置管理
- 敏感信息保护:使用密钥管理服务保护敏感配置
- 动态更新:实现配置热更新机制
- 验证机制:确保配置的正确性和完整性
在下一篇文章中,我们将探讨 API 版本与兼容性管理,这是保障系统演进的重要手段。