正在加载,请稍候…

Node.js 环境变量:正确处理配置与机密的最佳实践

学习如何在 Node.js 中正确管理环境变量,涵盖 dotenv、配置验证、机密管理、Docker 环境及安全最佳实践。

Node.js 环境变量:正确处理配置与机密的最佳实践

Node.js 环境变量:正确处理配置与机密的最佳实践

环境变量让你的应用知道它处于开发还是生产环境、数据库在哪里、以及使用哪些机密。如果处理不当,常常会导致 bug、安全漏洞和“在我机器上能跑”的问题。

基础:process.env

// 访问任何环境变量
const port = process.env.PORT;
const nodeEnv = process.env.NODE_ENV;
const dbUrl = process.env.DATABASE_URL;

// 始终为可选变量提供默认值
const port = process.env.PORT || 3000;
const logLevel = process.env.LOG_LEVEL || 'info';

Node.js 环境变量:正确处理配置与机密的最佳实践 插图

设置 dotenv

本地开发最常用的方法:

npm install dotenv
# .env 文件(项目根目录)
PORT=3000
NODE_ENV=development
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
JWT_SECRET=my-dev-secret-key
REDIS_URL=redis://localhost:6379
STRIPE_KEY=sk_test_xxxxx
// 在应用的最开始加载——在任何使用环境变量的其他导入之前
import 'dotenv/config';  // ESM
// 或者
require('dotenv').config(); // CommonJS

// 现在 process.env 已被填充
console.log(process.env.PORT); // 3000

关键:将 .env 添加到 .gitignore

# .gitignore
.env
.env.local
.env.*.local

多环境文件

.env              # 基础默认值(提交此文件——不含机密!)
.env.local        # 本地覆盖(不要提交)
.env.development  # 开发环境特定(如果不含机密可以提交)
.env.production   # 生产环境(不要提交——使用真正的机密管理器)
.env.test         # 测试环境
// 加载环境特定文件
const envFile = `.env.${process.env.NODE_ENV || 'development'}`;
require('dotenv').config({ path: envFile });

// 或者使用 dotenv-flow 自动处理
npm install dotenv-flow
require('dotenv-flow').config();

验证环境变量

绝不要让应用在缺少关键配置时启动!

// src/config/env.ts — 启动时验证
import { z } from 'zod';

const envSchema = z.object({
  // 必需
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  
  // 必需且限定值
  NODE_ENV: z.enum(['development', 'production', 'test']),
  
  // 可选且有默认值
  PORT: z.string().transform(Number).default('3000'),
  LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
  RATE_LIMIT_MAX: z.string().transform(Number).default('100'),
  
  // 可选
  REDIS_URL: z.string().url().optional(),
  SENTRY_DSN: z.string().url().optional(),
});

// 验证——如果任何地方出错则抛出异常
const parsed = envSchema.safeParse(process.env);

if (!parsed.success) {
  console.error('❌ 无效的环境变量:');
  console.error(parsed.error.flatten().fieldErrors);
  process.exit(1); // 立即停止应用
}

export const env = parsed.data;
// env.PORT 现在类型为 number,而非 string
// 在整个应用中使用
import { env } from './config/env';

app.listen(env.PORT, () => {
  console.log(`服务器运行在端口 ${env.PORT},模式为 ${env.NODE_ENV}`);
});

提供 .env.example 文件

始终提交一个模板,说明需要哪些变量:

# .env.example — 提交到版本控制
PORT=3000
NODE_ENV=development
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
JWT_SECRET=change-this-to-a-random-32-char-string
REDIS_URL=redis://localhost:6379

# 可选
SENTRY_DSN=
STRIPE_KEY=

新开发者运行 cp .env.example .env 并填入值。

Node.js 环境变量:正确处理配置与机密的最佳实践 插图

安全最佳实践

绝不记录机密

// ❌ 绝不要这样做
console.log('启动配置:', process.env);
console.log('数据库 URL:', process.env.DATABASE_URL); // 泄露凭据

// ✅ 只记录安全的值
console.log('启动模式:', process.env.NODE_ENV);
console.log('服务器端口:', process.env.PORT);
console.log('数据库已连接:', !!process.env.DATABASE_URL); // 仅确认存在

使用强机密

# 生成加密随机机密
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# 输出:a1b2c3d4e5f6...  (64 字符十六进制字符串)

# 或者使用你的令牌生成器工具

最小权限原则

# 为不同环境创建独立的数据库用户
# 开发:读写
# 生产:仅应用需要的权限
# CI/CD:可能只读用于测试运行

Node.js 环境变量:正确处理配置与机密的最佳实践 插图

Docker 和容器环境

# Dockerfile — 绝不硬编码机密
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .

# 不要在 Dockerfile 中使用 ENV 设置机密!
# 这些会被烘焙到镜像层中
# ENV JWT_SECRET=mysecret  ❌

EXPOSE 3000
CMD ["node", "dist/server.js"]
# docker-compose.yml — 使用 env_file 或 environment
services:
  api:
    build: .
    env_file:
      - .env          # 从文件加载
    environment:
      - NODE_ENV=production  # 覆盖特定值
    ports:
      - "3000:3000"

生产环境机密管理

对于生产环境,避免在服务器上使用 .env 文件。使用合适的机密管理器:

平台 工具
AWS Secrets Manager / Parameter Store
Google Cloud Secret Manager
Azure Key Vault
Kubernetes K8s Secrets + Sealed Secrets
Heroku/Railway 平台仪表盘环境变量
Doppler 同步机密到任何平台
// 示例:AWS Secrets Manager
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';

const client = new SecretsManagerClient({ region: 'us-east-1' });

async function getSecret(secretName) {
  const response = await client.send(
    new GetSecretValueCommand({ SecretId: secretName })
  );
  return JSON.parse(response.SecretString);
}

// 启动时
const secrets = await getSecret('myapp/production/database');
process.env.DATABASE_URL = secrets.url;

常见模式

类型安全的配置对象

// 集中配置——单一事实来源
export const config = {
  server: {
    port: Number(process.env.PORT) || 3000,
    env: process.env.NODE_ENV || 'development',
    isProduction: process.env.NODE_ENV === 'production',
  },
  database: {
    url: process.env.DATABASE_URL!,
    poolSize: Number(process.env.DB_POOL_SIZE) || 10,
  },
  auth: {
    jwtSecret: process.env.JWT_SECRET!,
    jwtExpiresIn: process.env.JWT_EXPIRES_IN || '7d',
  },
  redis: {
    url: process.env.REDIS_URL,
    enabled: !!process.env.REDIS_URL,
  },
} as const;

通过环境变量实现功能开关

const features = {
  enableBetaUI: process.env.ENABLE_BETA_UI === 'true',
  maxUploadSizeMB: Number(process.env.MAX_UPLOAD_SIZE_MB) || 10,
  maintenanceMode: process.env.MAINTENANCE_MODE === 'true',
};

if (features.maintenanceMode) {
  app.use((req, res) => {
    res.status(503).json({ message: '服务暂时不可用' });
  });
}

总结

规则 原因
绝不提交 .env 包含真实机密
始终提交 .env.example 记录所需配置
启动时验证 快速失败,错误清晰
生产环境使用机密管理器 安全、可审计、可轮换
绝不记录敏感值 日志会出现在许多地方
使用类型化配置对象 捕获拼写错误和类型错误

→ 使用 Token Generator 工具生成安全的随机机密。