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

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 の制限を理解し、適切に使い分けることが重要です。
実務で押さえるべきポイント
- Middleware は軽量処理に限定 (< 50ms)
- 署名検証・DB アクセスは Server Component / API Route へ
- matcher を適切に設定してパフォーマンスを最適化
- キャッシュ・メモ化を活用して外部 API 呼び出しを削減
- デバッグログを仕込んで問題を早期発見
受託開発で「認証がうまく動かない」「無限リダイレクトが起きる」といったトラブルの多くは、Middleware の使いどころを誤ったことが原因です。本記事の実装パターンとチェックリストを活用し、安全で高速な Web アプリケーションを構築してください。
Yureate では Next.js を使った Web アプリ開発を支援しています。 認証フロー・パフォーマンス最適化・セキュリティ対策まで、実務で培ったノウハウをもとに、安全で高速なアプリケーションを開発します。お気軽に お問い合わせ ください。
