Next.jsのMiddlewareについて学んでみる
What Middleware?
公式の解説を翻訳してみた👇
ミドルウェア
ミドルウェアを使用すると、リクエストが完了する前にコードを実行できます。その後、受信したリクエストに基づいて、リクエスト ヘッダーまたはレスポンス ヘッダーを書き換え、リダイレクト、変更したり、直接応答したりして、レスポンスを変更できます。
ミドルウェアは、キャッシュされたコンテンツとルートが一致する前に実行されます。詳細については、「一致するパス」を参照してください。
ユースケース
アプリケーションにミドルウェアを統合すると、パフォーマンス、セキュリティ、ユーザー エクスペリエンスが大幅に向上します。ミドルウェアが特に効果的な一般的なシナリオは次のとおりです。
認証と承認: 特定のページまたは API ルートへのアクセスを許可する前に、ユーザーの ID を確認し、セッション クッキーを確認します。
サーバー側リダイレクト: 特定の条件 (ロケール、ユーザー ロールなど) に基づいて、サーバー レベルでユーザーをリダイレクトします。
パスの書き換え: リクエストのプロパティに基づいて API ルートまたはページへのパスを動的に書き換えることで、A/B テスト、機能のロールアウト、またはレガシー パスをサポートします。
ボット検出: ボット トラフィックを検出してブロックすることでリソースを保護します。
ログ記録と分析: ページまたは API で処理する前に、リクエスト データをキャプチャして分析し、洞察を得ます。
機能フラグ設定: シームレスな機能のロールアウトやテストのために、機能を動的に有効または無効にします。
ミドルウェアが最適なアプローチではない可能性がある状況を認識することも同様に重要です。注意すべきシナリオをいくつか示します。
複雑なデータの取得と操作: ミドルウェアは直接的なデータの取得や操作用に設計されていないため、代わりにルート ハンドラーまたはサーバー側ユーティリティ内で実行する必要があります。
負荷の高い計算タスク: ミドルウェアは軽量で応答が速い必要があります。そうでないと、ページの読み込みに遅延が生じる可能性があります。負荷の高い計算タスクや長時間実行されるプロセスは、専用のルート ハンドラー内で実行する必要があります。
広範なセッション管理: ミドルウェアは基本的なセッション タスクを管理できますが、広範なセッション管理は専用の認証サービスまたはルート ハンドラー内で管理する必要があります。
直接データベース操作: ミドルウェア内で直接データベース操作を実行することは推奨されません。データベースのやり取りは、ルート ハンドラーまたはサーバー側ユーティリティ内で実行する必要があります。
Next.js Middleware Authentication Demo
このプロジェクトは、Next.jsのミドルウェア機能を使用した認証システムのデモンストレーションです。
機能概要
- ミドルウェアを使用した認証制御
- 保護されたルート(
/blog
)へのアクセス制御 - クッキーベースの認証状態管理
- ログイン/ログアウト機能
セットアップ
必要なパッケージをインストールします:
bun add js-cookie react-hot-toast @heroicons/react
bun add -d @types/js-cookie
ミドルウェアの実装について
このデモでは、middleware.ts
を使用して以下の認証制御を実装しています:
1. 認証チェック
- すべての保護されたルート(
/blog
)へのアクセスは認証が必要 - 未認証ユーザーは自動的にログインページにリダイレクト
- 認証済みユーザーがログインページにアクセスした場合は、ブログページにリダイレクト
2. 実装の詳細
// middleware.tsの主要な機能
- クッキーから認証トークンを確認
- パスに基づいた条件分岐
- 適切なリダイレクト処理
3. ミドルウェアの設定
export const config = {
matcher: ['/blog/:path*', '/login']
}
このmatcher設定により、ミドルウェアは以下のパスでのみ実行されます:
-
/blog
とその配下のすべてのパス -
/login
ページ
認証フロー
- 未認証ユーザーが保護されたページ(
/blog
)にアクセス → ログインページへリダイレクト - ログイン成功時 →
auth-token
クッキーが設定され、ブログページへリダイレクト - 認証済みユーザーがログインページにアクセス → ブログページへ自動リダイレクト
- ログアウト時 → クッキーを削除し、ログインページへリダイレクト
テスト用アカウント
デモ用のログイン情報:
- メールアドレス: hoge@co.jp
- 認証トークン: 10分で失効
技術スタック
- Next.js (App Router)
- TypeScript
- js-cookie (クッキー管理)
- react-hot-toast (通知UI)
- Tailwind CSS (スタイリング)
デモアプリのソースコードはこちらになります。
ログイン後のblog/
を作成
'use client'
import { useRouter } from 'next/navigation'
import Cookies from 'js-cookie'
import toast from 'react-hot-toast'
export default function Blog() {
const router = useRouter()
const handleLogout = () => {
Cookies.remove('auth-token')
toast.success('ログアウトしました')
router.push('/login')
}
return (
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
<div className="bg-white shadow-lg rounded-lg p-6">
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold text-gray-900">ブログページ</h1>
<button
onClick={handleLogout}
className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
>
ログアウト
</button>
</div>
<div className="prose max-w-none">
<p className="text-lg text-gray-700">
ここは認証が必要なブログページです。ログインしているユーザーのみがアクセスできます。
</p>
{/* ここにブログコンテンツを追加できます */}
</div>
</div>
</div>
</div>
)
}
ログインページのlogin/
のソースコード
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import Cookies from 'js-cookie'
import toast from 'react-hot-toast'
import { CheckCircleIcon } from '@heroicons/react/24/solid'
export default function Login() {
const [email, setEmail] = useState('')
const [loading, setLoading] = useState(false)
const router = useRouter()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
if (email === 'hoge@co.jp') {
// 10分後に失効するクッキーを設定
Cookies.set('auth-token', 'dummy-token', { expires: 1/144 })
// ローディング表示を少し見せるため
await new Promise(resolve => setTimeout(resolve, 1000))
toast.success('ログインしました', {
icon: <CheckCircleIcon className="w-6 h-6 text-green-500" />,
duration: 3000
})
router.push('/blog')
} else {
toast.error('メールアドレスが正しくありません')
setLoading(false)
}
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow-lg">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
ログイン
</h2>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
メールアドレス
</label>
<input
id="email"
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="hoge@co.jp"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
>
{loading ? (
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : (
'ログイン'
)}
</button>
</form>
</div>
</div>
)
}
最初に表示されるページのコードを修正。クリックしたらログインページへ遷移します。
import Link from "next/link";
export default function Home() {
return (
<div>
<h1>middleware demo</h1>
<p><Link href="/login">Login</Link></p>
</div>
);
}
middleware.ts
を作成して認証が通っているかいないかでページ遷移する設定をする。本来であればボタンをクリックしたらページ遷移するコードを書くより特定のページへは遷移できないようにしたり、認証がされていればページ遷移するようにするのが望ましい。
最近だとAuth.jsなるものを使うのも流行っているとか。
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const authToken = request.cookies.get('auth-token')
const isLoginPage = request.nextUrl.pathname === '/login'
// ログインページ以外へのアクセスで認証が必要
if (!authToken && !isLoginPage) {
return NextResponse.redirect(new URL('/login', request.url))
}
// すでにログインしている場合、ログインページにアクセスするとブログページにリダイレクト
if (authToken && isLoginPage) {
return NextResponse.redirect(new URL('/blog', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/blog/:path*', '/login']
}
まとめ
簡単ではなかったけど、Middlewareの使い方について学んでみました。公式の情報だけだと使い方を理解できなかったので、ダミーのデータを使って認証機能を仮実装してみました。
Discussion