---
title: "8.1 密钥管理与环境变量"
description: "保护你的 API Key 和数据库密码——.env、.gitignore、Server vs Client 边界、云端配置"
chapter: "第八章"
---

# 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 commit` 和 `git push`。密钥又上去了。他当时的心情大概是：我明明已经把密钥从代码里拿出来了，怎么还是泄露了？

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

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

# 依赖包（太大，队友自己装）
node_modules/

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

# 系统垃圾
.DS_Store
Thumbs.db
```

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

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

一个月后，小明换了台电脑。他从 GitHub 拉下代码，`pnpm install`，`pnpm 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_`，让它只在服务端可用。

<EnvBoundary />

## 改了配置没生效？先重启

小明改了 `.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 文件。如果有密钥写死在代码里，帮我改成读取环境变量。"

---

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

::: info 下一步
密钥管好了，接下来去 [认证方式与方案选择](./02-auth-methods.md)——了解 Session、Token、OAuth 等认证方式的区别，以及该选哪个认证库。
:::
