T-CREATOR

Next.js の Middleware 活用法:リクエスト制御・認証・リダイレクトの実践例

Next.js の Middleware 活用法:リクエスト制御・認証・リダイレクトの実践例

現代の Web アプリケーション開発において、リクエストの制御や認証、リダイレクト処理は必要不可欠な機能です。Next.js 12 から導入された Middleware 機能を活用することで、これらの処理をより効率的かつエレガントに実装できるようになりました。

本記事では、Next.js Middleware の基本概念から実践的な実装方法まで、段階的に解説いたします。リクエスト制御、認証システム、リダイレクト機能の具体的な実装例を通じて、皆さんのプロジェクトにすぐに活用できる知識をお届けします。

Next.js Middleware とは

Middleware の基本概念

Next.js Middleware は、リクエストが完了する前に実行されるコードです。この機能により、リクエストとレスポンスを変更し、リダイレクトやヘッダーの書き換え、認証チェックなどを行えます。

Middleware の最大の特徴は、Edge Runtimeで実行されることです。これにより、ユーザーに最も近いエッジサーバーで高速に処理が実行され、優れたパフォーマンスを実現します。

typescript// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  // リクエスト処理のロジック
  console.log('Middleware実行:', request.nextUrl.pathname);

  // レスポンスを返す
  return NextResponse.next();
}

上記のコードは最もシンプルな Middleware の例です。middleware.tsファイルをプロジェクトのルートに配置することで、すべてのリクエストに対してこの処理が実行されます。

リクエスト処理フローでの位置づけ

Next.js のリクエスト処理フローにおける Middleware の位置を図で確認してみましょう。

mermaidflowchart TD
  user[クライアント] -->|リクエスト| edge[Edge Runtime]
  edge -->|Middleware実行| middleware[Middleware処理]
  middleware -->|認証チェック| auth{認証OK?}
  auth -->|Yes| server[Next.jsサーバー]
  auth -->|No| redirect[リダイレクト]
  server -->|ページ生成| response[レスポンス]
  redirect -->|リダイレクト| response
  response -->|レスポンス| user

このフローからわかるように、Middleware はリクエストの最初の段階で実行されるため、不要なサーバーサイド処理を事前に防ぐことが可能です。認証が失敗した場合、サーバーでページを生成する前にリダイレクトできるため、パフォーマンスの向上にも貢献します。

従来の手法との比較

従来の Next.js では、認証チェックやリダイレクト処理を各ページコンポーネントで個別に実装する必要がありました。

従来の手法(getServerSideProps 使用)

typescript// pages/dashboard.tsx
export async function getServerSideProps(context) {
  const { req } = context;
  const token = req.cookies.token;

  // 認証チェック
  if (!token || !verifyToken(token)) {
    return {
      redirect: {
        destination: '/login',
        permanent: false,
      },
    };
  }

  return { props: {} };
}

Middleware 使用の場合

typescript// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('token');
  const isAuthPage =
    request.nextUrl.pathname.startsWith('/dashboard');

  if (isAuthPage && !token) {
    return NextResponse.redirect(
      new URL('/login', request.url)
    );
  }

  return NextResponse.next();
}

Middleware を使用することで、認証ロジックを一箇所に集約でき、各ページでの重複コードを削減できます。また、Edge Runtime での実行により、レスポンス速度も大幅に向上します。

リクエスト制御の実装

パスベースの制御

Middleware では、リクエストパスに基づいて異なる処理を実行できます。以下は管理者パネルへのアクセス制御の例です。

typescript// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 管理者パネルへのアクセス制御
  if (pathname.startsWith('/admin')) {
    return handleAdminAccess(request);
  }

  // API ルートの制御
  if (pathname.startsWith('/api/')) {
    return handleApiAccess(request);
  }

  return NextResponse.next();
}

function handleAdminAccess(request: NextRequest) {
  const userRole = request.cookies.get('userRole')?.value;

  if (userRole !== 'admin') {
    return NextResponse.redirect(
      new URL('/unauthorized', request.url)
    );
  }

  return NextResponse.next();
}

パスベースの制御により、アプリケーションの異なるセクションに対して適切なアクセス制御を実装できます。

条件分岐による制御

より複雑な条件分岐を使用して、きめ細かな制御を実装することも可能です。

typescriptfunction handleApiAccess(request: NextRequest) {
  const { pathname, searchParams } = request.nextUrl;
  const userAgent = request.headers.get('user-agent') || '';

  // モバイル向けAPI制限
  const isMobile = /mobile/i.test(userAgent);
  const isHeavyApi = pathname.includes('/heavy-operation');

  if (isMobile && isHeavyApi) {
    return NextResponse.json(
      {
        error:
          'この操作はモバイルではサポートされていません',
      },
      { status: 403 }
    );
  }

  // レート制限チェック
  const clientIp = request.ip || 'unknown';
  if (isRateLimited(clientIp)) {
    return NextResponse.json(
      { error: 'リクエスト数上限に達しました' },
      { status: 429 }
    );
  }

  return NextResponse.next();
}

この例では、ユーザーエージェントや IP アドレスに基づいて、動的にアクセス制御を行っています。

レスポンス変更とヘッダー操作

Middleware では、レスポンスのヘッダーを変更したり、新しいヘッダーを追加することも可能です。

typescriptexport function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // セキュリティヘッダーの追加
  response.headers.set('X-Content-Type-Options', 'nosniff');
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-XSS-Protection', '1; mode=block');

  // CORS設定
  const origin = request.headers.get('origin');
  const allowedOrigins = [
    'https://example.com',
    'https://app.example.com',
  ];

  if (allowedOrigins.includes(origin || '')) {
    response.headers.set(
      'Access-Control-Allow-Origin',
      origin || ''
    );
    response.headers.set(
      'Access-Control-Allow-Credentials',
      'true'
    );
  }

  // カスタムヘッダーの追加
  response.headers.set('X-Request-ID', generateRequestId());

  return response;
}

セキュリティヘッダーや CORS 設定を Middleware で一元管理することで、アプリケーション全体のセキュリティを向上させることができます。

認証機能の実装

JWT トークンによる認証

JWT トークンを使用した認証システムの実装例をご紹介します。

typescriptimport { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose';

const JWT_SECRET = new TextEncoder().encode(
  process.env.JWT_SECRET
);

export async function middleware(request: NextRequest) {
  // 保護されたルートの定義
  const protectedPaths = [
    '/dashboard',
    '/profile',
    '/settings',
  ];
  const isProtectedPath = protectedPaths.some((path) =>
    request.nextUrl.pathname.startsWith(path)
  );

  if (!isProtectedPath) {
    return NextResponse.next();
  }

  const token = request.cookies.get('token')?.value;

  if (!token) {
    return redirectToLogin(request);
  }

  try {
    const { payload } = await jwtVerify(token, JWT_SECRET);

    // トークンの有効期限チェック
    const currentTime = Math.floor(Date.now() / 1000);
    if (payload.exp && payload.exp < currentTime) {
      return redirectToLogin(request);
    }

    // ユーザー情報をヘッダーに追加
    const response = NextResponse.next();
    response.headers.set(
      'X-User-ID',
      payload.sub as string
    );
    response.headers.set(
      'X-User-Role',
      payload.role as string
    );

    return response;
  } catch (error) {
    console.error('JWT検証エラー:', error);
    return redirectToLogin(request);
  }
}

function redirectToLogin(request: NextRequest) {
  const loginUrl = new URL('/login', request.url);
  loginUrl.searchParams.set(
    'redirect',
    request.nextUrl.pathname
  );
  return NextResponse.redirect(loginUrl);
}

JWT トークンの検証を Middleware で行うことで、すべての保護されたページに対して統一的な認証チェックを実装できます。

セッションベース認証

セッションベースの認証システムも実装可能です。以下は Redis を使用したセッション管理の例です。

typescriptimport { NextRequest, NextResponse } from 'next/server';

// セッション検証関数(実際の実装では外部ファイルに分離)
async function validateSession(
  sessionId: string
): Promise<boolean> {
  try {
    // Redis や データベースでセッションを確認
    const response = await fetch(
      `${process.env.SESSION_API_URL}/validate`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ sessionId }),
      }
    );

    const result = await response.json();
    return result.valid;
  } catch (error) {
    console.error('セッション検証エラー:', error);
    return false;
  }
}

export async function middleware(request: NextRequest) {
  const sessionId = request.cookies.get('sessionId')?.value;
  const isProtectedRoute =
    request.nextUrl.pathname.startsWith('/protected');

  if (isProtectedRoute) {
    if (!sessionId) {
      return NextResponse.redirect(
        new URL('/login', request.url)
      );
    }

    const isValidSession = await validateSession(sessionId);

    if (!isValidSession) {
      // 無効なセッションの場合、クッキーをクリア
      const response = NextResponse.redirect(
        new URL('/login', request.url)
      );
      response.cookies.delete('sessionId');
      return response;
    }
  }

  return NextResponse.next();
}

セッションベース認証では、サーバーサイドでのセッション状態管理が必要ですが、よりセキュアな認証システムを構築できます。

認証失敗時の処理

認証失敗時の適切なエラーハンドリングは、ユーザビリティとセキュリティの両面で重要です。

typescriptexport async function middleware(request: NextRequest) {
  try {
    const authResult = await authenticateRequest(request);

    if (!authResult.success) {
      return handleAuthFailure(request, authResult.reason);
    }

    return NextResponse.next();
  } catch (error) {
    console.error('認証処理エラー:', error);
    return NextResponse.json(
      { error: 'サーバーエラーが発生しました' },
      { status: 500 }
    );
  }
}

function handleAuthFailure(
  request: NextRequest,
  reason: string
) {
  const isApiRequest =
    request.nextUrl.pathname.startsWith('/api/');

  if (isApiRequest) {
    // API リクエストの場合はJSONエラーを返す
    return NextResponse.json(
      {
        error: 'Unauthorized',
        message: '認証が必要です',
        code: 'AUTH_REQUIRED',
      },
      { status: 401 }
    );
  }

  // ページリクエストの場合はログインページにリダイレクト
  const loginUrl = new URL('/login', request.url);

  // エラー理由に応じてパラメータを設定
  switch (reason) {
    case 'expired':
      loginUrl.searchParams.set(
        'message',
        'セッションの有効期限が切れました'
      );
      break;
    case 'invalid':
      loginUrl.searchParams.set(
        'message',
        '認証情報が無効です'
      );
      break;
    default:
      loginUrl.searchParams.set(
        'message',
        'ログインが必要です'
      );
  }

  loginUrl.searchParams.set(
    'redirect',
    request.nextUrl.pathname
  );
  return NextResponse.redirect(loginUrl);
}

認証失敗の理由に応じて適切なメッセージを表示することで、ユーザーにとってわかりやすいエラーハンドリングを実現できます。

リダイレクト機能の実装

動的リダイレクト

ユーザーの状態や条件に基づいて動的にリダイレクト先を決定する機能を実装してみましょう。

mermaidflowchart TD
  request[リクエスト] --> auth{認証状態}
  auth -->|未認証| login[ログインページ]
  auth -->|認証済み| role{ユーザーロール}
  role -->|管理者| admin[管理画面]
  role -->|一般ユーザー| profile{プロファイル完了?}
  profile -->|未完了| setup[初期設定]
  profile -->|完了| dashboard[ダッシュボード]

上図のフローを実装した Middleware のコードは以下のようになります。

typescriptexport async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // ルートページへのアクセス時の動的リダイレクト
  if (pathname === '/') {
    return await handleRootRedirect(request);
  }

  return NextResponse.next();
}

async function handleRootRedirect(request: NextRequest) {
  const token = request.cookies.get('token')?.value;

  // 未認証の場合
  if (!token) {
    return NextResponse.redirect(
      new URL('/login', request.url)
    );
  }

  try {
    // ユーザー情報を取得
    const userInfo = await getUserInfo(token);

    // 管理者の場合
    if (userInfo.role === 'admin') {
      return NextResponse.redirect(
        new URL('/admin', request.url)
      );
    }

    // プロファイル未完了の場合
    if (!userInfo.profileComplete) {
      return NextResponse.redirect(
        new URL('/setup', request.url)
      );
    }

    // 通常ユーザーの場合
    return NextResponse.redirect(
      new URL('/dashboard', request.url)
    );
  } catch (error) {
    console.error('ユーザー情報取得エラー:', error);
    return NextResponse.redirect(
      new URL('/login', request.url)
    );
  }
}

このように動的リダイレクトを実装することで、ユーザーにとって最適なページに自動的に誘導できます。

条件付きリダイレクト

特定の条件下でのみリダイレクトを実行する機能も重要です。

typescriptexport function middleware(request: NextRequest) {
  const { pathname, searchParams } = request.nextUrl;
  const userAgent = request.headers.get('user-agent') || '';

  // モバイル向けリダイレクト
  if (shouldRedirectMobile(pathname, userAgent)) {
    return redirectToMobile(request);
  }

  // 地域別リダイレクト
  if (shouldRedirectByRegion(request)) {
    return redirectByRegion(request);
  }

  // メンテナンスモードのチェック
  if (
    isMaintenanceMode() &&
    !isMaintenanceBypass(request)
  ) {
    return NextResponse.redirect(
      new URL('/maintenance', request.url)
    );
  }

  return NextResponse.next();
}

function shouldRedirectMobile(
  pathname: string,
  userAgent: string
): boolean {
  const isMobile = /mobile|android|iphone/i.test(userAgent);
  const isDesktopOnlyPage = ['/admin', '/editor'].some(
    (path) => pathname.startsWith(path)
  );

  return isMobile && isDesktopOnlyPage;
}

function redirectToMobile(request: NextRequest) {
  const mobileUrl = new URL(
    '/mobile-not-supported',
    request.url
  );
  mobileUrl.searchParams.set(
    'original',
    request.nextUrl.pathname
  );
  return NextResponse.redirect(mobileUrl);
}

条件付きリダイレクトにより、デバイスや地域、システムの状態に応じた柔軟な制御が可能になります。

A/B テスト実装

Middleware を活用して A/B テストを実装することも可能です。

typescriptexport function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // A/Bテスト対象ページの場合
  if (pathname === '/landing') {
    return handleABTest(request);
  }

  return NextResponse.next();
}

function handleABTest(request: NextRequest) {
  const existingVariant =
    request.cookies.get('ab_variant')?.value;

  let variant: string;

  if (
    existingVariant &&
    ['A', 'B'].includes(existingVariant)
  ) {
    // 既存のバリアントを使用
    variant = existingVariant;
  } else {
    // 新規ユーザーにはランダムにバリアントを割り当て
    variant = Math.random() < 0.5 ? 'A' : 'B';
  }

  // バリアントに応じてリダイレクト
  const targetUrl =
    variant === 'A'
      ? new URL('/landing-a', request.url)
      : new URL('/landing-b', request.url);

  const response = NextResponse.redirect(targetUrl);

  // バリアント情報をクッキーに保存(30日間)
  response.cookies.set('ab_variant', variant, {
    maxAge: 30 * 24 * 60 * 60, // 30日
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
  });

  // 分析用のヘッダーを追加
  response.headers.set('X-AB-Variant', variant);

  return response;
}

A/B テストの実装により、ユーザー体験の改善に向けたデータドリブンな意思決定をサポートできます。

実践的な組み合わせパターン

認証とリダイレクトの連携

実際のアプリケーションでは、認証とリダイレクトを組み合わせた複雑な制御が必要になることがあります。

typescriptimport { NextRequest, NextResponse } from 'next/server';

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 各種設定の読み込み
  const config = {
    publicPaths: [
      '/login',
      '/register',
      '/forgot-password',
    ],
    adminPaths: ['/admin'],
    setupRequired: ['/dashboard', '/profile'],
  };

  // 認証チェック
  const authResult = await checkAuthentication(request);

  // 公開ページの場合は認証済みユーザーをリダイレクト
  if (
    config.publicPaths.includes(pathname) &&
    authResult.authenticated
  ) {
    return NextResponse.redirect(
      new URL('/dashboard', request.url)
    );
  }

  // 保護されたページの場合
  if (!config.publicPaths.includes(pathname)) {
    if (!authResult.authenticated) {
      return redirectToLogin(request, pathname);
    }

    // 管理者ページの場合、権限チェック
    if (
      config.adminPaths.some((path) =>
        pathname.startsWith(path)
      )
    ) {
      if (authResult.user?.role !== 'admin') {
        return NextResponse.redirect(
          new URL('/unauthorized', request.url)
        );
      }
    }

    // 初期設定が必要なページの場合
    if (
      config.setupRequired.some((path) =>
        pathname.startsWith(path)
      )
    ) {
      if (!authResult.user?.setupComplete) {
        return NextResponse.redirect(
          new URL('/setup', request.url)
        );
      }
    }
  }

  return NextResponse.next();
}

async function checkAuthentication(request: NextRequest) {
  const token = request.cookies.get('token')?.value;

  if (!token) {
    return { authenticated: false, user: null };
  }

  try {
    const user = await verifyToken(token);
    return { authenticated: true, user };
  } catch (error) {
    return { authenticated: false, user: null };
  }
}

このパターンにより、複雑な認証・認可要件を満たしつつ、適切なユーザー体験を提供できます。

エラーハンドリング

Middleware での包括的なエラーハンドリング戦略を実装してみましょう。

typescriptexport async function middleware(request: NextRequest) {
  try {
    return await processRequest(request);
  } catch (error) {
    return handleMiddlewareError(error, request);
  }
}

async function processRequest(request: NextRequest) {
  const startTime = Date.now();

  try {
    // 各種処理の実行
    const result = await executeMiddlewareLogic(request);

    // パフォーマンス測定
    const duration = Date.now() - startTime;

    // ログ出力
    console.log(
      `Middleware処理完了: ${request.nextUrl.pathname} (${duration}ms)`
    );

    return result;
  } catch (error) {
    // 処理時間を記録
    const duration = Date.now() - startTime;

    console.error(
      `Middleware処理エラー: ${request.nextUrl.pathname} (${duration}ms)`,
      {
        error: error.message,
        stack: error.stack,
        userAgent: request.headers.get('user-agent'),
        ip: request.ip,
      }
    );

    throw error;
  }
}

function handleMiddlewareError(
  error: Error,
  request: NextRequest
) {
  const isApiRequest =
    request.nextUrl.pathname.startsWith('/api/');

  if (isApiRequest) {
    return NextResponse.json(
      {
        error: 'Internal Server Error',
        message: 'システムエラーが発生しました',
        timestamp: new Date().toISOString(),
      },
      { status: 500 }
    );
  }

  // ページリクエストの場合はエラーページにリダイレクト
  const errorUrl = new URL('/error', request.url);
  errorUrl.searchParams.set('code', '500');
  return NextResponse.redirect(errorUrl);
}

適切なエラーハンドリングにより、システムの安定性とユーザビリティを向上させることができます。

パフォーマンス最適化

Middleware のパフォーマンスを最適化するためのベストプラクティスをご紹介します。

typescript// パフォーマンス最適化されたMiddleware
export async function middleware(request: NextRequest) {
  // 静的ファイルは早期リターン
  if (isStaticFile(request.nextUrl.pathname)) {
    return NextResponse.next();
  }

  // 並列処理でパフォーマンス向上
  const [authResult, configData] = await Promise.all([
    checkAuthenticationCached(request),
    getConfigCached(),
  ]);

  // 条件の早期評価
  if (shouldSkipProcessing(request, configData)) {
    return NextResponse.next();
  }

  return await processWithOptimization(
    request,
    authResult,
    configData
  );
}

// 静的ファイルの判定
function isStaticFile(pathname: string): boolean {
  const staticExtensions = [
    '.js',
    '.css',
    '.png',
    '.jpg',
    '.ico',
    '.svg',
  ];
  return staticExtensions.some((ext) =>
    pathname.endsWith(ext)
  );
}

// キャッシュ機能付き認証チェック
const authCache = new Map();
const CACHE_TTL = 60 * 1000; // 1分

async function checkAuthenticationCached(
  request: NextRequest
) {
  const token = request.cookies.get('token')?.value;

  if (!token) return { authenticated: false };

  // キャッシュから取得
  const cacheKey = `auth:${token.slice(-10)}`; // トークンの末尾10文字をキーに
  const cached = authCache.get(cacheKey);

  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
    return cached.data;
  }

  // 認証チェック実行
  const result = await verifyToken(token);

  // キャッシュに保存
  authCache.set(cacheKey, {
    data: result,
    timestamp: Date.now(),
  });

  return result;
}

このような最適化により、Middleware の処理速度を大幅に向上させることができます。

まとめ

Next.js Middleware は、現代の Web アプリケーション開発において非常に強力なツールです。本記事でご紹介した実装例を通じて、以下のような利点を活用できることがお分かりいただけたでしょう。

主要な利点

  • 統一的な制御: 認証やリダイレクトロジックを一箇所に集約
  • 高速処理: Edge Runtime による高速実行
  • 柔軟性: 複雑な条件分岐と動的な処理が可能
  • 保守性: コードの重複を削減し、メンテナンスが容易

実装のポイント

  • 早期リターンによるパフォーマンス最適化
  • 適切なエラーハンドリングの実装
  • キャッシュ機能の活用
  • セキュリティヘッダーの設定

Next.js Middleware を活用することで、よりセキュアで高性能な Web アプリケーションを構築していただけます。ぜひ皆さんのプロジェクトでも、これらの実装例を参考にして、Middleware の力を最大限に活用してください。

関連リンク