← 返回文章列表
Node.js·827 次阅读

Node.js 环境变量与配置管理:别再往代码里写死密钥了

上周做安全自查,在公司某个老仓库的提交历史里翻出了生产数据库的密码。虽然已经改过密码了,但这事提醒我:配置管理这个"低级问题",值得正经写一篇。

底线:密钥永远不进代码库

先立规矩,再谈技术:

  1. 任何密码、API Key、证书私钥,只存在于环境变量或密钥管理服务里
  2. .env 文件必须进 .gitignore,第一天就加
  3. 仓库里可以放 .env.example,列出需要哪些变量但值留空,给新人当文档
# .gitignore
.env
.env.local
.env.*.local
# .env.example —— 这个可以提交
DB_HOST=
DB_USER=
DB_PASSWORD=
JWT_SECRET=

dotenv:本地开发的标配

Node 本身不读 .env 文件(20.6 之前),用 dotenv:

// 入口文件最顶上,越早越好
require('dotenv').config()

// 之后 process.env 里就有 .env 文件里的值了
const db = createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
})

ESM 项目推荐这个写法,不用污染业务文件:

node --env-file=.env server.js   # Node 20.6+ 原生支持,dotenv 都可以省了

一个常见坑:dotenv.config() 必须在所有用到 env 的模块之前执行。如果模块 A 在文件顶部 require 时就读了 process.env.DB_HOST,而 dotenv 在模块 B 里才加载,A 拿到的就是 undefined。所以要么入口第一行加载,要么用 node -r dotenv/config server.js 预加载。

多环境怎么组织

开发、测试、生产三套环境的配置差异,常见三种方案:

方案一:单 .env + 环境变量覆盖(推荐小项目)

服务器上的真实配置通过部署平台注入(PM2 的 ecosystem 文件、Docker 的 -e、K8s 的 ConfigMap/Secret),.env 只在本地开发用:

// ecosystem.config.js(PM2)
module.exports = {
  apps: [{
    name: 'api',
    script: 'server.js',
    env_production: {
      NODE_ENV: 'production',
      DB_HOST: '10.0.1.20',
    },
  }],
}

方案二:分文件 .env.development / .env.production

按 NODE_ENV 加载不同文件。可用但容易乱,我不推荐——环境多了以后文件泛滥,且容易误提交。

方案三:配置中心(大团队)

Nacos、Apollo、云厂商的 Parameter Store。适合几十上百个微服务的场景,小团队别硬上。

配置校验:启动时就该炸

最怕的不是没配置,而是配置错了服务还能启动,半夜才在某个边缘路径上炸。

启动时校验一把,缺了什么、格式不对,直接拒启动:

// config.js
const required = ['DB_HOST', 'DB_USER', 'DB_PASSWORD', 'JWT_SECRET']

for (const key of required) {
  if (!process.env[key]) {
    console.error(`缺少必需的环境变量: ${key}`)
    process.exit(1)
  }
}

const config = {
  db: {
    host: process.env.DB_HOST,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    port: Number(process.env.DB_PORT || 3306),
  },
  jwtSecret: process.env.JWT_SECRET,
  port: Number(process.env.PORT || 3000),
}

// 数字校验:NaN 也拒启动
if (Number.isNaN(config.db.port) || Number.isNaN(config.port)) {
  console.error('DB_PORT / PORT 必须是数字')
  process.exit(1)
}

// 生产环境的弱密钥警告
if (process.env.NODE_ENV === 'production' && config.jwtSecret.length < 32) {
  console.error('生产环境 JWT_SECRET 长度至少 32 位')
  process.exit(1)
}

module.exports = config

业务代码全部从 config 模块取配置,不再散落 process.env——这样配置的来源和校验只有一处。

复杂项目可以上 zod 做 schema 校验,类型和默认值一并搞定:

const { z } = require('zod')

const envSchema = z.object({
  DB_HOST: z.string().min(1),
  DB_PORT: z.coerce.number().default(3306),
  JWT_SECRET: z.string().min(32),
  LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
})

const env = envSchema.parse(process.env) // 不合规直接抛错,启动失败

读取时的经典坑

env 值全是字符串:

if (process.env.CACHE_ENABLED) {
  // ❌ CACHE_ENABLED=false 也会进这里!"false" 是 truthy 字符串
}

// ✅ 正确姿势
const cacheEnabled = process.env.CACHE_ENABLED === 'true'

不要在客户端代码里用服务端的 env:用打包工具往前端注入变量时(Vite 的 VITE_*、Nuxt 的 public runtimeConfig),带前缀的才会进客户端包。往客户端注入数据库密码的事故真实发生过。

小结

  • 密钥只走环境变量,.env 进 .gitignore,.env.example 进仓库
  • Node 20.6+ 可以 node --env-file=.env,少一个依赖
  • dotenv 必须最先加载,否则模块级读取拿不到值
  • 启动时校验配置,错了就拒启动;业务代码统一从 config 模块取
  • env 值都是字符串,布尔判断用 === 'true'

配置管理没有高深技术,全是纪律。但线上事故的半壁江山,恰恰都倒在这些"低级纪律"上。

看到这里了?如果这篇文章帮到过你

请我喝瓶可乐就好 🥤

收款码

评论0 条

还没有评论,来抢沙发~

发表评论

评论需审核后显示

© 2026 许冬博客 · 专注 JS 技术分享

·RSS 订阅·Powered by Nuxt 4