← ブログ一覧

Next.js Middleware で認証・リダイレクト・ロギングを実装する実践ガイド

Next.js 15 Middleware の使いどころを整理。認証チェック・リダイレクト・A/Bテスト・ロギングの実装パターン、パフォーマンス最適化、トラブルシューティングまで網羅した実務ガイド。

#Next.js#TypeScript#Web#セキュリティ
Next.js Middleware で認証・リダイレクト・ロギングを実装する実践ガイド

Next.js Middleware で認証・リダイレクト・ロギングを実装する実践ガイド

Next.js の Middleware は、リクエストが完了する前にコードを実行できる強力な機能です。認証チェック、リダイレクト、A/B テスト、ロギングなど、多くのユースケースで活躍しますが、「どこまで Middleware でやるべきか」 の判断が難しいのが実情です。

本記事では、受託開発・自社開発の現場で即使える Middleware の実装パターンを、パフォーマンス・保守性・セキュリティの観点から整理します。


1. Middleware の基本と実行タイミング

Middleware が実行されるタイミング

Middleware は Edge Runtime で実行され、以下のタイミングで動作します。

  • すべてのルート(app/ 配下)へのリクエスト前
  • 静的ファイル(public/)、_next/static除外される
  • API Routes・Server Actions・Page の前に実行

実行順序の例

1. Middleware 実行 (Edge Runtime)
2. リダイレクト or 次のステップへ
3. Page / API Route / Server Action (Node.js Runtime)

Middleware でできること・できないこと

| できること | できないこと | |---|---| | Cookie の読み書き | データベース直接接続 | | ヘッダーの追加・変更 | Node.js API(fs, path など) | | リダイレクト・書き換え | 複雑な暗号化処理 | | A/B テスト用の分岐 | 重い計算処理 | | ロギング(軽量) | サードパーティ SDK の多く |


2. 認証チェックの実装パターン

パターン 1: Cookie ベース認証チェック

セッション Cookie を確認し、未認証ユーザーをログインページにリダイレクトする基本パターンです。

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

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

  // 認証が必要なパス
  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    if (!sessionCookie) {
      // ログインページにリダイレクト
      const loginUrl = new URL('/login', request.url)
      loginUrl.searchParams.set('from', request.nextUrl.pathname)
      return NextResponse.redirect(loginUrl)
    }
  }

  return NextResponse.next()
}

export const config = {
  matcher: [
    '/dashboard/:path*',
    '/api/:path*',
  ],
}

パターン 2: JWT 検証(軽量)

JWT の署名検証は重いため、Middleware では最小限のチェックにとどめます。

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

function isValidJWT(token: string): boolean {
  try {
    // 簡易的な形式チェックのみ(署名検証は Server Component で)
    const parts = token.split('.')
    if (parts.length !== 3) return false
    
    const payload = JSON.parse(
      Buffer.from(parts[1], 'base64').toString()
    )
    
    // 有効期限チェック
    if (payload.exp && payload.exp * 1000 < Date.now()) {
      return false
    }
    
    return true
  } catch {
    return false
  }
}

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth_token')?.value

  if (request.nextUrl.pathname.startsWith('/admin')) {
    if (!token || !isValidJWT(token)) {
      return NextResponse.redirect(new URL('/login', request.url))
    }
  }

  return NextResponse.next()
}

推奨設計:

  • Middleware: 形式・有効期限の軽量チェック
  • Server Component / API Route: 署名検証・権限チェック

パターン 3: 外部認証サービスとの連携

Auth0・Clerk・Supabase などを使う場合、SDK が提供する Middleware を利用します。

// middleware.ts (Clerk の例)
import { authMiddleware } from '@clerk/nextjs'

export default authMiddleware({
  publicRoutes: ['/', '/about', '/api/webhook'],
  ignoredRoutes: ['/api/public/:path*'],
})

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
}

3. リダイレクト・書き換えの実装パターン

パターン 1: ロケールベースリダイレクト

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const locales = ['en', 'ja', 'zh']
const defaultLocale = 'ja'

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
  
  // パスにロケールが含まれているかチェック
  const pathnameHasLocale = locales.some(
    (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  )

  if (pathnameHasLocale) return NextResponse.next()

  // Accept-Language ヘッダーから言語を取得
  const acceptLanguage = request.headers.get('accept-language')
  const preferredLocale = acceptLanguage?.split(',')[0].split('-')[0]
  const locale = locales.includes(preferredLocale || '') 
    ? preferredLocale 
    : defaultLocale

  // ロケールを含むパスにリダイレクト
  request.nextUrl.pathname = `/${locale}${pathname}`
  return NextResponse.redirect(request.nextUrl)
}

export const config = {
  matcher: ['/((?!_next|api|favicon.ico).*)'],
}

パターン 2: メンテナンスモード切り替え

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const MAINTENANCE_MODE = process.env.MAINTENANCE_MODE === 'true'
const ALLOWED_IPS = process.env.ALLOWED_IPS?.split(',') || []

export function middleware(request: NextRequest) {
  if (!MAINTENANCE_MODE) return NextResponse.next()

  const pathname = request.nextUrl.pathname
  if (pathname === '/maintenance') return NextResponse.next()

  // 許可された IP アドレスはスキップ
  const ip = request.headers.get('x-forwarded-for') || request.ip
  if (ip && ALLOWED_IPS.includes(ip)) {
    return NextResponse.next()
  }

  // メンテナンスページにリダイレクト
  return NextResponse.redirect(new URL('/maintenance', request.url))
}

パターン 3: A/B テスト用の書き換え

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname
  
  // トップページの A/B テスト
  if (pathname === '/') {
    let variant = request.cookies.get('ab_test_variant')?.value
    
    if (!variant) {
      // 新規訪問者: ランダムに A/B を割り当て
      variant = Math.random() < 0.5 ? 'A' : 'B'
      const response = NextResponse.rewrite(
        new URL(variant === 'A' ? '/' : '/variant-b', request.url)
      )
      response.cookies.set('ab_test_variant', variant, {
        maxAge: 60 * 60 * 24 * 30, // 30日
      })
      return response
    }
    
    // 既存訪問者: Cookie の値に従う
    if (variant === 'B') {
      return NextResponse.rewrite(new URL('/variant-b', request.url))
    }
  }

  return NextResponse.next()
}

4. ロギング・分析の実装パターン

パターン 1: 基本的なアクセスログ

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const startTime = Date.now()
  
  const response = NextResponse.next()
  
  // レスポンスヘッダーに処理時間を追加
  response.headers.set('X-Response-Time', `${Date.now() - startTime}ms`)
  
  // ログ出力(本番では外部ロギングサービスへ送信)
  console.log(JSON.stringify({
    timestamp: new Date().toISOString(),
    method: request.method,
    url: request.url,
    userAgent: request.headers.get('user-agent'),
    referer: request.headers.get('referer'),
  }))
  
  return response
}

パターン 2: エラー追跡

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
  try {
    // Middleware のロジック
    return NextResponse.next()
  } catch (error) {
    // Sentry などにエラーを送信
    console.error('Middleware error:', {
      error: error instanceof Error ? error.message : 'Unknown error',
      url: request.url,
      method: request.method,
    })
    
    // エラーページにリダイレクト
    return NextResponse.redirect(new URL('/error', request.url))
  }
}

5. パフォーマンス最適化

最適化ポイント一覧

| 項目 | 推奨 | 避けるべき | |---|---|---| | 実行時間 | < 50ms | > 200ms | | 外部 API 呼び出し | キャッシュ利用 | 毎回呼び出し | | データベース | 使わない | 直接接続 | | 計算処理 | 最小限 | 暗号化・画像処理 | | matcher 設定 | 必要なパスのみ | /:path* の乱用 |

matcher の最適化

// ❌ 悪い例: すべてのリクエストで実行
export const config = {
  matcher: '/:path*',
}

// ✅ 良い例: 必要なパスのみ
export const config = {
  matcher: [
    '/dashboard/:path*',
    '/api/protected/:path*',
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
}

キャッシュの活用

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// メモリキャッシュ(Edge Runtime では制限あり)
const cache = new Map<string, { value: any; expiry: number }>()

function getCached<T>(key: string): T | null {
  const item = cache.get(key)
  if (!item) return null
  if (item.expiry < Date.now()) {
    cache.delete(key)
    return null
  }
  return item.value as T
}

function setCache<T>(key: string, value: T, ttlMs: number): void {
  cache.set(key, {
    value,
    expiry: Date.now() + ttlMs,
  })
}

export async function middleware(request: NextRequest) {
  const userId = request.cookies.get('user_id')?.value
  if (!userId) return NextResponse.next()

  const cacheKey = `user:${userId}`
  let userPermissions = getCached<string[]>(cacheKey)

  if (!userPermissions) {
    // 外部 API から取得(本番では Vercel KV などを使用)
    const response = await fetch(`https://api.example.com/users/${userId}/permissions`)
    userPermissions = await response.json()
    setCache(cacheKey, userPermissions, 5 * 60 * 1000) // 5分
  }

  // 権限チェック
  if (!userPermissions.includes('admin')) {
    return NextResponse.redirect(new URL('/unauthorized', request.url))
  }

  return NextResponse.next()
}

6. トラブルシューティング

よくある問題と解決策

| 問題 | 原因 | 解決策 | |---|---|---| | 無限リダイレクト | リダイレクト先も matcher に一致 | リダイレクト先を matcher から除外 | | 静的ファイルが取得できない | matcher が広すぎる | _next/static, public を除外 | | Cookie が設定されない | Response を正しく返していない | NextResponse.next() / redirect() を使用 | | 環境変数が読めない | Edge Runtime の制限 | NEXT_PUBLIC_ プレフィックスを使用 | | Middleware が実行されない | matcher の設定ミス | matcher の正規表現を確認 |

デバッグ用ログ出力

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  if (process.env.NODE_ENV === 'development') {
    console.log('🔍 Middleware Debug:', {
      pathname: request.nextUrl.pathname,
      method: request.method,
      cookies: Object.fromEntries(
        request.cookies.getAll().map(c => [c.name, c.value])
      ),
      headers: Object.fromEntries(
        Array.from(request.headers.entries())
      ),
    })
  }

  return NextResponse.next()
}

7. 実務での使い分け判断フロー

認証チェックが必要?
├─ Yes → Middleware で Cookie 確認
│         ├─ 軽量チェックのみ (形式・有効期限)
│         └─ 署名検証は Server Component へ
└─ No → 次へ

リダイレクトが必要?
├─ Yes → Middleware でリダイレクト
│         ├─ ロケール・メンテナンス: Middleware
│         └─ ビジネスロジック: Server Component
└─ No → 次へ

A/B テスト・ロギング?
├─ Yes → Middleware で軽量処理
│         ├─ Cookie 設定・書き換え: Middleware
│         └─ 詳細分析: API Route
└─ No → Middleware 不要

判断基準チェックリスト

Middleware を使うべきケース

  • [ ] すべてのリクエストで実行したい
  • [ ] Cookie / Header の読み書きのみ
  • [ ] リダイレクト・書き換えが必要
  • [ ] 処理時間が 50ms 以内

Server Component / API Route を使うべきケース

  • [ ] データベースアクセスが必要
  • [ ] 複雑なビジネスロジック
  • [ ] 重い計算・暗号化処理
  • [ ] サードパーティ SDK を使う

8. セキュリティ考慮事項

CSRF 対策

Middleware で CSRF トークンを検証する例:

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']

export function middleware(request: NextRequest) {
  // API リクエストの CSRF チェック
  if (request.nextUrl.pathname.startsWith('/api/')) {
    if (!SAFE_METHODS.includes(request.method)) {
      const csrfToken = request.headers.get('x-csrf-token')
      const cookieToken = request.cookies.get('csrf_token')?.value

      if (!csrfToken || csrfToken !== cookieToken) {
        return NextResponse.json(
          { error: 'Invalid CSRF token' },
          { status: 403 }
        )
      }
    }
  }

  return NextResponse.next()
}

レート制限

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// 簡易的なレート制限(本番では Vercel KV / Upstash Redis を使用)
const requestCounts = new Map<string, { count: number; resetAt: number }>()

const RATE_LIMIT = 100 // 1分間に100リクエスト
const WINDOW_MS = 60 * 1000

export function middleware(request: NextRequest) {
  const ip = request.headers.get('x-forwarded-for') || request.ip || 'unknown'
  const now = Date.now()
  
  let record = requestCounts.get(ip)
  
  if (!record || record.resetAt < now) {
    record = { count: 0, resetAt: now + WINDOW_MS }
    requestCounts.set(ip, record)
  }
  
  record.count++
  
  if (record.count > RATE_LIMIT) {
    return NextResponse.json(
      { error: 'Too many requests' },
      { 
        status: 429,
        headers: {
          'Retry-After': String(Math.ceil((record.resetAt - now) / 1000)),
        },
      }
    )
  }
  
  const response = NextResponse.next()
  response.headers.set('X-RateLimit-Limit', String(RATE_LIMIT))
  response.headers.set('X-RateLimit-Remaining', String(RATE_LIMIT - record.count))
  
  return response
}

まとめ

Next.js Middleware は、認証チェック・リダイレクト・A/B テスト・ロギングなど幅広いユースケースで活躍しますが、Edge Runtime の制限を理解し、適切に使い分けることが重要です。

実務で押さえるべきポイント

  1. Middleware は軽量処理に限定 (< 50ms)
  2. 署名検証・DB アクセスは Server Component / API Route へ
  3. matcher を適切に設定してパフォーマンスを最適化
  4. キャッシュ・メモ化を活用して外部 API 呼び出しを削減
  5. デバッグログを仕込んで問題を早期発見

受託開発で「認証がうまく動かない」「無限リダイレクトが起きる」といったトラブルの多くは、Middleware の使いどころを誤ったことが原因です。本記事の実装パターンとチェックリストを活用し、安全で高速な Web アプリケーションを構築してください。


Yureate では Next.js を使った Web アプリ開発を支援しています。 認証フロー・パフォーマンス最適化・セキュリティ対策まで、実務で培ったノウハウをもとに、安全で高速なアプリケーションを開発します。お気軽に お問い合わせ ください。

この内容について相談する他の記事を見る