【Next.js】middleware.tsでログイン必須ページを保護する

JavaScript

こんにちは、かつコーチです。

前回はAuth.jsを使って、Next.jsアプリにログイン・ログアウト機能を実装しました。

ただしAuth.jsのauth()をページごとに呼び出す方式だと、保護したいページが増えるたびにチェック処理を書き足すことになり、書き漏らしのリスクも出てきます。

今回はmiddleware.tsを使って、ログイン必須ページをまとめて保護する方法を解説します。

middleware.tsとは?前提知識

リクエストがページに届く前に処理を挟む仕組み

middlewareとは、リクエストがルーティングされる前に割り込んで処理を実行できる仕組みです。

プロジェクトルート(またはsrc直下)にmiddleware.tsを1つ配置するだけで、指定したパスへのすべてのリクエストに対して共通処理を差し込めます。

認証チェック以外にも、リダイレクト、ロケール判定、Botのブロックなどにも使われます。

Edge Runtimeで動作する点に要注意

middlewareが実行時に依存するのはEdge Runtimeです。

Edge Runtimeは軽量・高速に動く反面、Node.jsの標準APIの一部が使えないという制約があります。

具体的には、ファイルシステムを扱うfsモジュールや、一部のデータベースクライアントなど、Node.js固有のAPIに依存するライブラリはmiddleware内では動作しません。

「middlewareで急に動かないコードがある」と感じたら、まずEdge Runtimeの制約を疑うのが定石です。

基本の書き方・実装手順

手順1:middleware.tsの作成

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { auth } from "@/auth";

export default auth((req: NextRequest & { auth: unknown }) => {
  const isLoggedIn = !!req.auth;
  const isAuthPage = req.nextUrl.pathname.startsWith("/login");

  if (!isLoggedIn && !isAuthPage) {
    const loginUrl = new URL("/login", req.nextUrl.origin);
    return NextResponse.redirect(loginUrl);
  }

  return NextResponse.next();
});

Auth.jsが提供するauth関数でmiddlewareをラップすることで、Edge Runtime上でもセッション情報を扱えます。

未ログインの状態で保護対象ページにアクセスしてきたら、/loginにリダイレクトする流れです。

手順2:適用対象パスをmatcherで絞り込む

// middleware.ts(末尾に追記)
export const config = {
  matcher: [
    "/dashboard/:path*",
    "/settings/:path*",
    "/mypage/:path*",
  ],
};

matcherを指定しないと、静的ファイルへのリクエストも含めてすべてのパスにmiddlewareが走ってしまい、無駄な処理とパフォーマンス低下につながります。

保護したいページのプレフィックスだけを明示的に指定しておくのが安全です。

手順3:ログイン済みユーザーをログインページから追い出す

// middleware.ts(分岐を追加)
if (isLoggedIn && isAuthPage) {
  return NextResponse.redirect(new URL("/dashboard", req.nextUrl.origin));
}

ログイン済みなのに/loginにアクセスしてきた場合は、ダッシュボードなど適切なページへ逆方向にリダイレクトします。

こうしておくと「ログイン済みなのにログイン画面が表示される」という不自然なUXを防げます。

つまずきやすい設定・注意点

matcherに指定するパスパターンは、静的解析できる範囲の記法しかサポートされていません。

変数を使った動的なパス生成はできないため、保護対象が増える見込みがあるなら、あらかじめ上位のディレクトリ単位(/dashboard/:path*など)でまとめて指定しておくのがおすすめです。

よくあるつまずきポイント・エラー対処

実際にハマった「Module not found: Can’t resolve ‘fs’」

私が実際につまずいた例を1つ共有します。

ログイン状態のチェックに加えて、middleware内でDBに問い合わせて権限情報を確認しようとしたところ、次のエラーでビルドが止まりました。

Module not found: Can't resolve 'fs'

利用していたORMクライアントが内部でNode.jsのfsモジュールに依存していたため、Edge Runtime上では読み込めなかったのが原因でした。

❌ Before:middleware内でNode.js依存のORMを直接呼び出す

// middleware.ts
import { db } from "@/lib/db"; // Node.js依存のORMクライアント

export default auth(async (req) => {
  const user = await db.user.findUnique({ where: { id: req.auth?.userId } });
  // ...
});

✅ After:middlewareではセッション情報だけで判定し、詳細な権限チェックはページ側に任せる

// middleware.ts
export default auth((req) => {
  const isLoggedIn = !!req.auth;
  // ログイン有無の判定だけをmiddlewareで行う
  if (!isLoggedIn) {
    return NextResponse.redirect(new URL("/login", req.nextUrl.origin));
  }
  return NextResponse.next();
});
// app/dashboard/admin/page.tsx側で詳細な権限チェックを行う
import { db } from "@/lib/db";
import { auth } from "@/auth";
import { redirect } from "next/navigation";

export default async function AdminPage() {
  const session = await auth();
  const user = await db.user.findUnique({ where: { id: session?.user?.id } });

  if (user?.role !== "admin") {
    redirect("/dashboard");
  }

  return <div>管理者ページ</div>;
}

middlewareは「ログインしているかどうか」のような軽量な判定に留め、DBアクセスを伴う詳細な権限チェックはServer Component側に任せる役割分担にすることで、Edge Runtimeの制約に振り回されずに済みます。

まとめ

この記事のポイント

  • middlewareはリクエストがページに届く前に共通処理を挟める仕組みで、認証チェックの一括管理に向いている
  • middlewareはEdge Runtimeで動作するため、fsなどNode.js固有のAPIに依存するライブラリは使えない
  • config.matcherで適用対象パスを絞り込むことで、不要な処理を避けパフォーマンスを保てる
  • DBアクセスを伴う詳細な権限チェックは、middlewareではなくページ側のServer Componentに任せるのが安全

次に読むべき記事

認証・保護のロジックが書けるようになったら、次はそのロジックが壊れていないことを保証する仕組みが必要です。

次回は、Next.jsアプリにVitestとPlaywrightでテストを導入する方法を解説します。

→ 次の記事:Next.jsのテスト入門:Vitest + Playwrightで最低限を押さえる

タイトルとURLをコピーしました