第159章:練習:環境変数の漏えいを防ぐ(公開/非公開の区別)🔒✨
今日は「環境変数(env)をうっかり漏らさない」練習をするよ〜!🥹🫶 Next.jsは便利だけど、設定ミスすると秘密がブラウザに出ちゃうことがあるの…!😱💦
この章のゴール🎯
- ✅ 公開してOKなenv と 絶対に秘密なenv を区別できる
- ✅ Next.jsで「秘密がクライアントに出ない」書き方ができる
- ✅ 「漏れてないかチェック」までできる🔍✨
まず覚えるルール3つ📌(ここ超重要!)
NEXT_PUBLIC_が付いたenvはブラウザに配信される(=公開) 🌍- 秘密(APIキーなど)は
NEXT_PUBLIC_を付けない 🔑❌ - サーバーで読んでも、画面に表示したら漏えい(HTMLに乗る) 🫣💥
図でイメージしよ〜🧠🪄(公開/非公開の流れ)
ハンズオン🧪:公開envと秘密envを分けて使ってみる💡
0) .env.local を用意する🔧
プロジェクト直下に .env.local を作って、こう書いてね👇(値は例だよ〜)
NEXT_PUBLIC_SITE_TITLE="Campus TODO 🌸"
PAYMENT_API_KEY="sk_test_123456789_secret"
📝 envを変えたら
npm run devは再起動してね!(反映されないこと多いよ〜)🔁✨
1) envを「public用」「server用」で分ける📦✨
src/env/public.ts(公開OKだけ)
export const publicEnv = {
siteTitle: process.env.NEXT_PUBLIC_SITE_TITLE ?? "My App",
} as const;
src/env/server.ts(秘密だけ:クライアントに絶対持っていかせない)
import "server-only";
export const serverEnv = {
paymentApiKey: process.env.PAYMENT_API_KEY ?? "",
} as const;
if (!serverEnv.paymentApiKey) {
// 開発中は気づけるように、空ならエラーにしちゃうのもアリ👍
console.warn("⚠️ PAYMENT_API_KEY が未設定だよ!");
}
import "server-only"; を入れると、うっかりクライアント側からimportしたときに事故りにくくなるよ🛡️✨(好き!)
2) 公開envを「クライアントコンポーネント」で表示してみる🌸
app/_components/SiteTitle.tsx
"use client";
import { publicEnv } from "@/env/public";
export function SiteTitle() {
return (
<h1 style={{ fontSize: 24, fontWeight: 700 }}>
{publicEnv.siteTitle} 🎀
</h1>
);
}
3) 秘密envは「サーバーだけ」で使って、結果だけ返す🔐
API(Route Handler)を作る:app/api/secure/route.ts
「秘密キーが設定されてるか」だけ返すよ(キー自体は絶対返さない!)🙅♀️🔑
import { NextResponse } from "next/server";
import { serverEnv } from "@/env/server";
export async function GET() {
const configured = serverEnv.paymentApiKey.length > 0;
return NextResponse.json({
configured,
message: configured ? "OK! 🔐" : "未設定だよ…🥲",
});
}
4) クライアントから安全にチェックしてみる👀✨
app/_components/SecretCheckClient.tsx
"use client";
import { useEffect, useState } from "react";
type ApiResponse = {
configured: boolean;
message: string;
};
export function SecretCheckClient() {
const [data, setData] = useState<ApiResponse | null>(null);
useEffect(() => {
fetch("/api/secure")
.then((r) => r.json())
.then((json: ApiResponse) => setData(json))
.catch(() => setData({ configured: false, message: "取得失敗…😵💫" }));
}, []);
if (!data) return <p>確認中…⏳</p>;
return (
<p>
サーバー秘密キーの状態:{" "}
<b>{data.configured ? "設定済み✅" : "未設定❌"}</b> / {data.message}
</p>
);
}
5) ページに置く🏠✨(Server Component)
app/page.tsx
import { SiteTitle } from "./_components/SiteTitle";
import { SecretCheckClient } from "./_components/SecretCheckClient";
export default function Page() {
return (
<main style={{ padding: 24, lineHeight: 1.8 }}>
<SiteTitle />
<hr style={{ margin: "16px 0" }} />
<p>このページは「env漏えいしない練習」だよ〜🫶🔒</p>
<SecretCheckClient />
<p style={{ marginTop: 16 }}>
※ 秘密キーはサーバー側でだけ使って、ブラウザには出さないのが正解✨
</p>
</main>
);
}
🚨 絶対やっちゃダメ集(ここ試験に出る)😱🧨
❌ 1) 秘密envに NEXT_PUBLIC_ を付ける
NEXT_PUBLIC_PAYMENT_API_KEY="sk_test_..."
これ、そのままブラウザに配られる可能性が高いよ…!終わり!😭💥
❌ 2) クライアントで秘密envを読もうとする
"use client";
console.log(process.env.PAYMENT_API_KEY);
秘密は「ブラウザで見えていい」世界じゃないよ〜🙅♀️🔑
❌ 3) サーバーで読んだ秘密を“返す/表示する”
return NextResponse.json({ key: serverEnv.paymentApiKey }); // ←即アウト😇
漏れてないかチェックする🔍✨(Windows向け)
PAYMENT_API_KEY の値(例:sk_test_123456789_secret)が、ビルド成果物に混ざってないか探すよ!
PowerShellで探す🪟
npm run build
Get-ChildItem .next\static -Recurse -Filter *.js | Select-String -Pattern "sk_test_123456789_secret"
- ✅ 何も出なければOK 🎉
- ❌ 出たらどこかで漏れてる!(
NEXT_PUBLIC_や、クライアントimport事故を疑う)😱
仕上げチェックリスト✅🔒
- 公開していいものだけ
NEXT_PUBLIC_にした?🌍 - 秘密キーを 画面/JSON/ログ に出してない?🙅♀️
- 秘密を読むモジュールは
server-onlyを入れた?🛡️ -
.env.localをGitに入れてない?(基本入れない!)🫣 - ビルド後に
.next/staticを検索しても秘密が出ない?🔍✨
ミニ課題🎓✨(この章の練習)
/api/secureのレスポンスにhint: "configured"みたいな 安全な情報だけを足してみてね😊- 逆に、秘密そのものを返したらどう危険かを言葉で説明してみてね🧠💬
ここまでできたら、envの「公開/非公開」かなり強いよ〜!🥳🔒✨