C
Next.js/ルーティング/Lesson 11

Middleware — すべてのリクエストを横断して認証・リダイレクト・ヘッダー操作

30分·theory
このチャプター
3/3
TypeScript

Middleware — すべてのリクエストを横断して認証・リダイレクト・ヘッダー操作

💡 なぜ学ぶ必要があるのか — ページコンポーネントごとに if(!session) を書かないために

🎯 保護されたページごとに「セッションがなければログインへ転送」するコードを書く必要がなく、ミドルウェア1か所にまとめるだけで完結します。
💼 Edge Runtime で実行されるため、ページコンポーネントに到達する前にリクエストをブロックします。より速いレスポンスを実現します。
matcher config を使って、どのルートに適用するかを正確に制御できます。
🔗 A/Bテスト、国別リダイレクト、ボットブロック、レスポンスヘッダーの追加など、ルーティング直前に必要なすべてのロジックを処理できます。
📈 codemaster40のNext.jsアプリも [`src/middleware.ts`](src/middleware.ts) を使って `/dashboard`・`/admin` などの保護されたルートを処理しています。
🏢 실무에서는
このプロジェクトのmiddleware.tsはまさにその役割を担っています — `/dashboard`・`/bookmark`・`/memo-notes` などのパーソナライズされたルートへのアクセス時にセッションを検証し、セッションがなければ `/login?next=...` へリダイレクトします。ページコンポーネントはセッションが存在することを前提として実装できます。

middleware.ts · matcher · NextResponse

1. 場所とシグネチャ

code
src/middleware.ts  ← src/app と同じ階層(またはプロジェクトルート)
ts
import { NextResponse, type NextRequest } from 'next/server';

export function middleware(req: NextRequest) {
  // リクエストごとに実行
  return NextResponse.next();  // そのまま通過
}

export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*'],
};
  • Edge Runtime で実行(一部の Node API は制限あり)。
  • matcher に記述したパスにのみ適用。

2. matcher パターン

ts
export const config = {
  matcher: [
    '/dashboard/:path*',           // /dashboard/* すべて
    '/((?!api|_next|favicon).*)',  // api/_next/favicon を除くすべて
  ],
};

3. 主要な動作 4 種

ts
// (a) そのまま通過
return NextResponse.next();

// (b) 別パスへ rewrite(URL はそのまま、内部ルーティングのみ変更)
return NextResponse.rewrite(new URL('/landing', req.url));

// (c) リダイレクト(URL 自体を変更)
return NextResponse.redirect(new URL('/login', req.url));

// (d) レスポンスヘッダーの操作
const res = NextResponse.next();
res.headers.set('x-custom', 'value');
return res;

4. 実践 — 認証 Middleware

ts
import { NextResponse, type NextRequest } from 'next/server';

export function middleware(req: NextRequest) {
  const session = req.cookies.get('session')?.value;

  if (!session) {
    const loginUrl = new URL('/login', req.url);
    loginUrl.searchParams.set('next', req.nextUrl.pathname);
    return NextResponse.redirect(loginUrl);
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/bookmark/:path*', '/memo-notes/:path*'],
};

5. レスポンスの Cookie・ヘッダー操作

ts
export function middleware(req: NextRequest) {
  const res = NextResponse.next();

  // レスポンス Cookie を設定
  res.cookies.set('lastVisit', new Date().toISOString(), {
    httpOnly: true,
    secure: true,
  });

  // セキュリティヘッダーを追加
  res.headers.set('X-Frame-Options', 'DENY');
  res.headers.set('X-Content-Type-Options', 'nosniff');

  return res;
}

6. Edge Runtime の制約

  • fsnetdns などの Node 専用モジュールは使用不可。
  • レスポンスタイムは 25ms 以内を推奨(Vercel 基準)。
  • 重い DB 呼び出し禁止 — 軽量な検証のみ。
💻 🅰️ ページごとの認証検証 — コードの重複が爆発
// ❌ ミドルウェアなし — ページごとに認証コードを繰り返し

// 📁 app/dashboard/page.tsx
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';

export default async function Dashboard() {
  const session = cookies().get('session')?.value;
  if (!session) redirect('/login?next=/dashboard');
  // ... ページ本文
}

// 📁 app/bookmark/page.tsx
export default async function Bookmark() {
  const session = cookies().get('session')?.value;
  if (!session) redirect('/login?next=/bookmark');
  // ... 同じコード
}

// 📁 app/memo-notes/page.tsx
export default async function MemoNotes() {
  const session = cookies().get('session')?.value;
  if (!session) redirect('/login?next=/memo-notes');
  // ... また同じコード
}

// 欠点:
// - ページごとに4行重複
// - 新しいページを追加する際に忘れやすい
// - 認証ポリシーが変わると全てのページを修正する必要がある
// - ページコンポーネントが実行されて初めて検証される → 若干のコンパイル・転送オーバーヘッド
💻 🅱️ Middleware — すべての保護ルートを一箇所で処理
// ✅ middlewareで統合 — ページコンポーネントには認証コードなし

// 📁 src/middleware.ts
import { NextResponse, type NextRequest } from 'next/server';

const PROTECTED_ROUTES = ['/dashboard', '/bookmark', '/memo-notes'];
const ADMIN_ROUTES = ['/admin'];

export function middleware(req: NextRequest) {
  const session = req.cookies.get('session')?.value;
  const path = req.nextUrl.pathname;

  // 1. 保護された経路 → 未ログインならログインページへ
  if (PROTECTED_ROUTES.some(p => path.startsWith(p))) {
    if (!session) {
      const loginUrl = new URL('/login', req.url);
      loginUrl.searchParams.set('next', path);
      return NextResponse.redirect(loginUrl);
    }
  }

  // 2. 管理者経路 → 別途権限検証
  if (ADMIN_ROUTES.some(p => path.startsWith(p))) {
    const adminToken = req.cookies.get('admin')?.value;
    if (!adminToken) {
      return NextResponse.redirect(new URL('/admin/login', req.url));
    }
  }

  // 3. 応答ヘッダー — セキュリティ強化
  const res = NextResponse.next();
  res.headers.set('X-Frame-Options', 'DENY');
  res.headers.set('X-Content-Type-Options', 'nosniff');
  return res;
}

export const config = {
  matcher: [
    // _next/static·_next/image·favicon·apiを除く全ての経路
    '/((?!_next/static|_next/image|favicon.ico|api).*)',
  ],
};

// 📁 app/dashboard/page.tsx — これでクリーン
export default async function Dashboard() {
  // session検証コードなし — ミドルウェアが既にブロック
  // ここに到達したなら認証済みユーザーと確定
  const data = await db.dashboard.find();
  return <div>{/* ... */}</div>;
}

// 利点:
// - 保護ポリシーを一箇所に集中
// - 新しい保護ページ = PROTECTED_ROUTES配列に追加するだけ
// - Edge Runtime → 高速応答 (ページコンポーネントに到達する前にブロック)
// - ページコンポーネントがクリーン (認証ロジックX、ビジネスロジックのみ)

💡 💡 Middleware 実践 Tips 5 選

1. matcher に否定先読みパターン — 「除外」の表現

ts
matcher: ['/((?!_next/static|_next/image|favicon|api).*)']

頻繁な静的アセットリクエストを Middleware が処理しないようにする。

2. Edge Runtime の制約 — 重い DB 呼び出し禁止
Cookie 検証・シンプルな JWT デコードなど軽量な処理のみ。実際の DB クエリはページコンポーネントで行う。

3. NextResponse.next() でヘッダーをまとめて設定

ts
const res = NextResponse.next();
res.headers.set('x-foo', 'bar');
return res;

4. レスポンス Cookie の設定

ts
res.cookies.set('name', 'value', { httpOnly: true, secure: true });

httpOnly + secure がデフォルト。SameSite=Lax も推奨。

5. デバッグ — console.log はサーバーログへ
Middleware 内の console.log は Vultr・Vercel のサーバーログに出力される。ブラウザのコンソールには表示されない。

⚡ 自分で試してみよう — Middleware 動作シミュレーション

各リクエストパスが Middleware でどのように処理されるかをシミュレーションする。
✏️ JS 코드
📟 コンソール出力
▶ 実行ボタンを押してください
⚠️ ブラウザのサンドボックスで実行 — console.log()のみ対応、alert/fetchは不可

確認クイズ

middleware.ts の matcher 設定で実現できることはどれか?
💡 `matcher` はミドルウェアが**どのルートに適用されるか**を定義します。頻繁な静的アセットリクエスト(`_next/static`・`_next/image`・`favicon`)やAPIリクエストをミドルウェアが処理しないよう、否定先読みパターンで除外するのが一般的です。matcher を指定しない場合はすべてのルートに適用され、パフォーマンスへの影響が大きくなります。
Middleware — 認証・リダイレクト・ヘッダー - Next.js