Skip to content

第 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;
  }
}

创业团队行动清单

  1. 立即行动

    • 建立多环境配置管理机制
    • 将敏感信息从代码中移除
    • 实现基本的配置验证
  2. 一周内完成

    • 集成密钥管理服务(如 Vault 或 AWS KMS)
    • 实现配置热更新机制
    • 添加配置变更监控
  3. 一月内完善

    • 建立完整的配置中心
    • 实现配置版本管理和回滚
    • 添加配置审计日志

总结

配置管理是保障系统安全和灵活性的重要手段,正确的实现需要:

  1. 环境隔离:建立清晰的多环境配置管理
  2. 敏感信息保护:使用密钥管理服务保护敏感配置
  3. 动态更新:实现配置热更新机制
  4. 验证机制:确保配置的正确性和完整性

在下一篇文章中,我们将探讨 API 版本与兼容性管理,这是保障系统演进的重要手段。