--- 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_`,让它只在服务端可用。 ## 改了配置没生效?先重启 小明改了 `.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 等认证方式的区别,以及该选哪个认证库。 :::