跳到主要内容

4 篇博文 含有标签「dotenv」

查看所有标签

dotenv 值被 # 静默截断?.env 变量加双引号包裹

· 阅读需 4 分钟

在开发 AI运营 时遇到此问题——基于大语言模型的智能分析,自动洞察市场趋势、用户行为、销售数据,提供精准运营策略。

TL;DR

dotenv 把未加引号的值中 # 当作行内注释。KEY=value#hash 实际加载的值是 value#hash 被丢弃且无任何报错。解法:.env 中含 #、空格、特殊字符的值,一律用双引号包裹 —— KEY="value#hash"

问题现象

后端调用上游服务,一直返回 401 Invalid credentials

POST /api/v1/dag/trigger → 500
日志堆栈:Airflow JWT auth failed (401): {"detail":"Invalid credentials"}
at getJwtToken (airflow-client.ts)

排查发现 .env 文件中写的密码是 24 位、含 #&

AIRFLOW_PASSWORD=ooGR0^kThVI&ag#RyCpUmbIr

但 Node.js 进程实际加载到的 process.env.AIRFLOW_PASSWORD 长度只有 10,#RyCpUmbIr 整段消失。用 CLAUDE.md 记录的完整密码直接 curl 上游鉴权接口 → 返回 201;用 .env 解析出来的残缺值 → 返回 401。密码账号没问题,是 .env 加载出来的值被截断了

根因

dotenv 沿用 shell 习惯,把未加引号的值中 # 之后的内容视为行内注释:

# .env
AIRFLOW_PASSWORD=ooGR0^kThVI&ag#RyCpUmbIr
# dotenv 实际解析为:
# AIRFLOW_PASSWORD = "ooGR0^kThVI&ag"
# #RyCpUmbIr ← 被丢弃

这个行为符合 dotenv 文档,但没有任何警告或日志,运行时拿到的就是个静默截断的字符串。叠加 &、空格、$ 等字符的 shell 转义语义,问题更隐蔽:

字符未加引号时的行为
#之后内容当行内注释,截断
(空格)之后内容被丢弃
$VAR触发变量展开(可能为空字符串)
&shell 后台符号,在 dotenv 中通常保留但在 shell 拼接命令时再次踩坑

JWT_SECRETAPI_KEYDATABASE_URL 这类强随机串经常包含 #,是高发雷区。

解决方案

.env 中含特殊字符的值用双引号包裹:

# .env
AIRFLOW_PASSWORD="ooGR0^kThVI&ag#RyCpUmbIr"
JWT_SECRET="abc#def$ghi jkl"
DATABASE_URL="postgres://user:p@ss#word@host:5432/db"

重启服务使新值生效:

# pm2
pm2 restart analytics-api --update-env

# docker compose
docker compose restart api

# systemd
sudo systemctl restart api

为什么有效:dotenv 看到双引号包裹后,会按字面值读取到右引号为止,#、空格、$ 都不会被特殊处理(除非显式开启 expand 选项)。改完后立即验证加载结果:

// 启动时验证关键变量长度,提前拦截截断
const required = ['AIRFLOW_PASSWORD', 'JWT_SECRET', 'DATABASE_URL'] as const;
for (const key of required) {
const v = process.env[key];
if (!v || v.length < 16) {
throw new Error(`${key} 未正确加载(长度 ${v?.length ?? 0}),请检查 .env 引号`);
}
}

这样把 dotenv 的静默失败转成启动失败,下次踩坑时第一时间暴露。

注意事项

  • 单引号也能用,但 dotenv 在单引号内不展开 $VAR,双引号会展开。涉及密码通常希望字面值,建议双引号 + 不写 ${...}
  • dotenv 版本:v15+ 默认行为如上;早期版本(v8 之前)对 # 的处理略有差异,升级前先看 CHANGELOG。
  • Docker / Kubernetes Secret:通过 environment: 注入的变量不走 dotenv,不受此坑影响;只有 .env 文件、dotenv.config() 路径才受影响。
  • CI 环境:GitHub Actions、GitLab CI 把 secret 注入到 env 上下文,也不走 dotenv。

常见问题

为什么 .env 中含 # 的密码会变短?

dotenv 默认把 # 之后的内容当作行内注释丢弃。未加引号的值 KEY=value#hash 实际加载为 value,没有报错也没有日志。把值用双引号包裹 KEY="value#hash" 即可保留完整内容。

dotenv 不生效怎么排查?

三步:先确认 dotenv.config() 在所有 import 之前执行(ES Module 的 import 是静态提升的,详见 JWT 签名静默失败排查);再确认 .env 值不含未转义的 # 或空格;最后在进程启动后打印 process.env.XXX 的长度与字符,与 .env 源文件逐一比对。

CCLEE

独立开发者,24年电商行业实战经验,专注将AI能力落地于真实商业场景。

合作咨询

JWT 签名静默失败?检查 Node.js 环境变量的加载顺序

· 阅读需 3 分钟

在为客户构建 SaaS 认证系统时遇到此问题,记录根因与解法。

TL;DR

Node.js ES Module 中,import 语句在 dotenv.config() 之前执行。如果模块级代码读取 process.env.JWT_SECRET,拿到的是 undefined,导致 JWT 签名用 "undefined" 字符串作为密钥——不报错,但所有 token 校验都失败。解决方案:延迟初始化(lazy init)。

问题现象

JWT 登录接口返回 200,但后续请求全部 401。排查发现:

  1. 登录生成的 token 无法被 jwtVerify() 验证
  2. 每次重启服务,之前签发的 token 全部失效
  3. 打印 process.env.JWT_SECRET,结果是 undefined
// jwt.ts — 模块级代码
import crypto from 'crypto';

// ❌ 这行在 dotenv.config() 之前执行,JWT_SECRET 是 undefined
const SECRET = crypto.createSecretKey(
new TextEncoder().encode(process.env.JWT_SECRET)
);

最坑的是:不报错new TextEncoder().encode(undefined) 会把字符串 "undefined" 编码成字节,生成一个合法但错误的密钥。

根因

ES Module 的 import静态提升的:

// server.ts(入口文件)
import { router } from './routes/auth'; // ← 先执行
import { authenticateToken } from './middleware/auth'; // ← 先执行

dotenv.config(); // ← 后执行,但 import 链已经跑完了

执行顺序:

  1. Node.js 扫描所有 import,构建依赖图
  2. 深度优先执行所有被导入模块的顶层代码(jwt.tsconst SECRET = ... 在这里执行)
  3. 回到 server.ts,执行 dotenv.config()
  4. 此时 .env 才加载到 process.env

所以 jwt.ts 模块级代码读到的 process.env.JWT_SECRETundefined

解决方案

方案一:延迟初始化(推荐)

把密钥初始化从模块级移到函数内部,首次调用时才读取环境变量:

import crypto from 'crypto';

let _secret: crypto.KeyObject | null = null;

function getSecret(): crypto.KeyObject {
if (!_secret) {
const secretValue = process.env.JWT_SECRET;
if (!secretValue) {
throw new Error('JWT_SECRET 环境变量未设置');
}
_secret = crypto.createSecretKey(
new TextEncoder().encode(secretValue)
);
}
return _secret;
}

// 所有需要密钥的地方改用 getSecret()
export async function generateToken(payload: any): Promise<string> {
return new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.sign(getSecret()); // ← 延迟到运行时读取
}

优势:不依赖入口文件的 import 顺序,任何调用时机都安全。

方案二:入口文件顶部调用 dotenv

// server.ts — 确保这两行在所有 import 之前
import 'dotenv/config'; // 或 require('dotenv').config()
import express from 'express';
// ...其他 import

局限:如果有其他入口文件(如 cron job、worker)忘记加这行,问题复现。

注意事项

注意事项

  • 这个坑不只影响 JWT,所有模块级代码读取环境变量都有同样风险(数据库连接、API Key 等);ESM 模块解析还有另一个常见坑——动态 import 缺少 .js 扩展名会导致生产环境模块找不到
  • require('dotenv').config() 只在 CommonJS 中能保证顺序;ES Module 中 import 始终先于运行时代码
  • 延迟初始化模式适用于:密钥、数据库连接池、外部 API 客户端等一次性资源;如果你的 JWT 使用 jose 库且升级了 Node 24,也要注意 jose KeyObject 格式变化