⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议
Skip to content

8.1 密钥管理与环境变量

本节目标:学会用环境变量管理密钥,理解 Server 与 Client 的安全边界,确保敏感信息永远不出现在代码和 Git 历史中。


小明的教训

序言里小明把 API Key 推到了 GitHub,5 分钟内被盗刷了 $127。但故事要从更早说起。

小明最初写代码时,数据库连接是这样的:

typescript
const db = drizzle('postgresql://ming:mypassword123@db.example.com:5432/moviedb')

AI 密钥是这样的:

typescript
const openai = new OpenAI({ apiKey: 'sk-proj-abc123...' })

他觉得这样挺方便——代码里直接写,不用到处找。而且本地跑得好好的,也没出过问题。直到他把代码推到 GitHub,密钥被盗刷,他才明白一个道理:代码是要分享的,密钥是不能分享的。把密钥写在代码里,就像把银行卡密码写在卡背面——只要别人看到你的卡,密码就暴露了。

这里说的"密钥"包括很多东西:数据库连接字符串(DATABASE_URL)里藏着数据库的用户名和密码,第三方 API Key(OPENAI_API_KEY)是你调用 AI 服务的凭证,OAuth 密钥(GITHUB_CLIENT_SECRET)是你和 GitHub 之间的信任凭证,支付密钥(STRIPE_SECRET_KEY)直接关联你的收款账户,认证密钥(BETTER_AUTH_SECRET)用来签名用户的 Session。它们的共同点是:一旦泄露,轻则被盗刷额度,重则用户数据全部暴露。而且这些密钥往往是互相关联的——数据库密码泄露意味着用户表暴露,用户表暴露意味着所有人的邮箱和密码哈希都被拿走了。

解决方案是环境变量——把密钥从代码中抽出来,放到一个不会被提交的文件里。代码里只写"去某个地方读密钥",而不是把密钥本身写进去。

从硬编码到 .env

老师傅教小明的第一件事:在项目根目录创建一个 .env 文件,把所有密钥集中放进去:

DATABASE_URL=postgresql://user:password@host:5432/mydb
OPENAI_API_KEY=sk-proj-xxxxx
BETTER_AUTH_SECRET=your-random-secret-here

然后代码里不再写死密钥,而是通过 process.env.DATABASE_URL 读取——就像手机 App 不会把屏幕亮度写死在代码里,而是去"系统设置"里读取当前值。process.env 就是 Node.js 的"系统设置",它会自动读取 .env 文件里的所有键值对。这样一来,密钥和代码彻底分离——代码可以放心推到 GitHub,密钥留在本地的 .env 文件里。这就像把家里的钥匙从门垫下面(写在代码里)搬到保险箱里(放在 .env 里)。门垫下面谁都能翻到,保险箱至少需要知道密码才能打开。

但光有 .env 还不够。小明创建了 .env 文件,把密钥都搬了进去,觉得万事大吉。然后他习惯性地执行了 git add .——这个命令会把当前目录下所有文件都加入 Git 暂存区,包括 .env。他没注意到这一点,直接 git commitgit push。密钥又上去了。他当时的心情大概是:我明明已经把密钥从代码里拿出来了,怎么还是泄露了?

老师傅指着项目根目录下的 .gitignore 文件说:"这才是防止你破产的最后一道防线。" .gitignore 是 Git 的屏蔽清单——凡是写在这个文件里的名字,Git 在扫描项目变动时都会自动忽略,git add . 也加不进去。你可以把它理解为一份"黑名单":上了名单的文件,Git 假装看不见。检查你的 .gitignore 是否包含以下内容:

gitignore
# 敏感配置
.env
.env.local
.env.production

# 依赖包(太大,队友自己装)
node_modules/

# 构建产物(编译生成的临时文件)
.next/
dist/

# 系统垃圾
.DS_Store
Thumbs.db

已经提交过的文件,加 .gitignore 没用

如果 .env 已经被 Git 追踪过(之前提交过),单纯加到 .gitignore 不会让它消失。你需要先执行 git rm --cached .env 移除缓存,再提交一次。更严重的是,Git 历史里仍然能看到之前提交的密钥——告诉 AI:"我不小心把密钥提交到 Git 历史里了,帮我清洗掉。"清洗完之后,一定要去对应平台重置密钥

.env.example——给未来的自己留个模板

一个月后,小明换了台电脑。他从 GitHub 拉下代码,pnpm installpnpm dev——一堆报错。DATABASE_URL 未定义、OPENAI_API_KEY 未定义... 他才想起来:.env 被屏蔽了,新电脑上根本没有这个文件。更尴尬的是,他已经记不清项目需要哪些环境变量了。这个项目是三个月前开始的,中间加了 AI 功能、加了认证、加了第三方 OAuth,每次加功能都往 .env 里塞了新变量,但他从来没记录过。翻代码找 process.env. 的引用?太麻烦了,而且可能漏掉。

老师傅说:"你应该在项目里放一个 .env.example 文件。"这个文件列出所有变量名但不填值:

DATABASE_URL=
OPENAI_API_KEY=
BETTER_AUTH_SECRET=
NEXT_PUBLIC_APP_URL=

.env.example 要提交到 Git。它就像一个空白表格——拿到项目的人复制一份改名为 .env,填上自己的值就能跑。如果说 .env 是装着真钥匙的保险箱,.env.example 就是一个空的钥匙模具,告诉你需要配几把什么样的钥匙。

跟 AI 说:"帮我根据项目中用到的所有环境变量,生成一个 .env.example 文件。"

Server vs Client——最容易踩的坑

小明在 .env 里配了 API_KEY=sk-proj-xxx,在 API Route(服务端代码)里 console.log(process.env.API_KEY) 能打印出来。但他想在前端页面上显示一个"AI 功能已启用"的标记,于是在 React 组件里也读了一下这个变量——undefined。他检查了三遍拼写,怀疑是 bug。又试了 console.log(process.env),发现前端能读到的环境变量里根本没有 API_KEY

他把这个问题丢给 AI,AI 解释说:这不是 bug,是 Next.js 的安全机制

想象一下:你的后端代码运行在你自己的服务器上,就像银行的金库——只有内部员工能进去。但前端代码最终会被下载到用户的浏览器里运行,就像银行大厅——所有人都能看到。如果你把金库的密码贴在大厅的墙上,那金库就形同虚设了。

Server(服务端) 运行在云端服务器或你本地的 Node.js 进程上。用户看到的只是服务器返回的结果(HTML、JSON),无法直接接触服务器内部的代码和变量。所以在这里读取密钥是安全的——.env 里的变量默认只在服务端可用。Client(客户端) 运行在用户的浏览器里。你写的 React 组件、CSS、JavaScript 最终都会被打包发送到用户的浏览器。用户按 F12 打开开发者工具,就能看到所有发送到浏览器的代码和数据。如果密钥被发到客户端,等于把保险箱密码贴在了大门上——任何人都能看到。

所以 Next.js 的规则很简单:只有加了 NEXT_PUBLIC_ 前缀的变量,才会被打包发送到浏览器。这个前缀就像行李箱上的"可托运"标签——没贴标签的行李,航空公司不会帮你送上飞机;没加前缀的变量,Next.js 不会帮你送到浏览器。比如 DATABASE_URL 没有这个前缀,只在 Server 可用,适合存数据库密码、API 密钥等敏感信息;而 NEXT_PUBLIC_APP_URL 有这个前缀,Server 和 Client 都能读到,适合存网站地址、统计 ID 等公开信息。这个前缀就是一个"我确认这个值可以公开"的声明——Next.js 看到这个前缀,才会把变量的值打包进前端代码。判断标准也很简单:问自己,如果用户在浏览器 F12 里看到这个值,有没有安全风险?有风险就不加 NEXT_PUBLIC_,让它只在服务端可用。

🔒服务端 (安全)
DATABASE_URL=postgres://user:pass@host/db
AUTH_SECRET=super-secret-key
STRIPE_SECRET_KEY=sk_live_...
安全边界
🌐客户端 (公开)
NEXT_PUBLIC_APP_URL=https://myapp.com
NEXT_PUBLIC_STRIPE_KEY=pk_live_...
服务端变量客户端代码
泄露风险:在客户端代码中使用服务端变量会导致密钥暴露
规则NEXT_PUBLIC_ 前缀 = 客户端可见,不带前缀 = 仅服务端

改了配置没生效?先重启

小明改了 .env 里的数据库密码(因为旧密码泄露了,他重置了一个新的),刷新页面,发现还是报"连接失败"。他又改了一遍,还是不行。他开始怀疑是不是 .env 文件坏了。老师傅淡淡地说了一句:"重启了吗?"

小明含泪 Ctrl+C 停掉开发服务器,重新 pnpm dev,一切正常。

原因很简单:环境变量在程序启动时加载到内存里。程序运行期间,它读的是内存里的旧值,不会自动去检查 .env 文件有没有变化。这就像你出门前看了一眼天气预报说晴天,出门后天气变了,但你不会知道——除非你再看一次。这个坑几乎每个人都会踩一次。90% 的 .env 问题,重启就能解决。

从本地到云端

小明的应用终于要部署上线了。他把代码推到 Vercel,点了部署按钮,满怀期待地等着看线上效果。结果所有功能都挂了——数据库连不上、AI 接口报错、认证失败。他这才意识到一个被忽略的问题:本地开发用的是 .env 文件,但 .env.gitignore 屏蔽了,根本不会被推到 GitHub,Vercel 的服务器上自然也没有这个文件。代码在那里运行时,process.env.DATABASE_URL 读到的是 undefined

每个部署平台(Vercel、Railway、Cloudflare 等)都有专门的环境变量设置页面。你需要在那里把本地 .env 里的内容一条条填进去——变量名和值都要填,就像在一个在线表单里重新录入一遍。这就像把钱从家里的保险箱(本地 .env)转移到银行的保险箱(云端配置)——位置变了,但本质没变:密钥存在安全的地方,代码通过 process.env 读取。填的时候要注意:有些值在本地和生产环境是不同的,比如 DATABASE_URL 本地可能指向 localhost,生产环境要指向云端数据库的地址;BETTER_AUTH_URL 本地是 http://localhost:3000,生产环境要改成你的域名。

同样的道理,如果你使用了 MCP 服务器(比如 Supabase MCP、GitHub MCP),它们的连接信息也包含敏感凭证,同样要通过环境变量管理,不要写死在配置文件里。

跟 AI 说:"检查我的项目,确保所有密钥都通过环境变量管理。生成 .gitignore 和 .env.example 文件。如果有密钥写死在代码里,帮我改成读取环境变量。"


本节核心要点

  • 密钥放 .env,代码里用 process.env.XXX 读取
  • .gitignore 屏蔽 .env,防止提交到 Git
  • .env.example 列出变量名不填值,提交到 Git 当模板
  • 不加 NEXT_PUBLIC_ 前缀的变量只在服务端可用——这是安全机制,不是 bug
  • 改了 .env 要重启服务
  • 部署时在平台的环境变量页面配置,不要上传 .env 文件

下一步

密钥管好了,接下来去 认证方式与方案选择——了解 Session、Token、OAuth 等认证方式的区别,以及该选哪个认证库。