Next.js JWT 認証を簡単に: セットアップからデプロイまで
Emily Parker
Product Engineer · Leapcell

Next.js での JWT ミドルウェアによる認証と認可の実装:基礎から実践的な応用まで
I. はじめに:認証に JWT を選択する理由
現代のウェブ開発では、ユーザー認証と認可はセキュアなアプリケーションを構築する上で中心的な側面です。JSON Web Token (JWT) は、ステートレス、クロスプラットフォーム、軽量という特性を持ち、フロントエンドとバックエンドが分離されたアプリケーションにおいて、最も主流な認証ソリューションの 1 つとなっています。React エコシステムで最も人気のあるフルスタックフレームワークである Next.js は、リクエストのインターセプトとルート保護を効率的に実装できる強力なミドルウェアメカニズムを提供します。この記事では、Next.js でカスタムミドルウェアと JWT を組み合わせてユーザー認証を実現する方法について詳しく説明し、リクエストに有効な userid
と username
が含まれるようにし、基本原則から本番レベルの実践まで、プロセス全体を網羅します。
II. JWT の基礎:コアコンセプトと動作原理
2.1 JWT 構造の分析
JWT は、.
で区切られた次の 3 つの部分で構成されています。
- ヘッダー: トークンの種類 (JWT) と署名アルゴリズム (HMAC SHA256、RSA など) が含まれています。
{ "alg": "HS256", "typ": "JWT" }
- ペイロード: ユーザー情報 (機密性の低いデータ) とメタデータ (有効期限
exp
など) が保存されます。{ "sub": "1234567890", // ユーザーの一意の識別子 (userid) "name": "John Doe", // ユーザー名 (username) "iat": 1687756800, // 発行時刻 "exp": 1687760400 // 有効期限 (1 時間後) }
- 署名: ヘッダーで指定されたアルゴリズムを使用してヘッダーとペイロードに署名し、トークンが改ざんされていないことを保証します。
2.2 認証プロセス
- ログインフェーズ: ユーザーが認証情報 (ユーザー名/パスワード) を送信します。サーバーが正常に検証した後、JWT を生成してクライアントに返します。
- トークンのストレージ: クライアントは、JWT を
HttpOnly
Cookie またはローカルストレージに保存します (XSS 攻撃を回避するために Cookie が推奨されます)。 - リクエストのインターセプト: 後続の各リクエストは、JWT を (通常は
Authorization
ヘッダーまたは Cookie で) 伝送します。サーバーは、ミドルウェアを介してトークンの有効性を検証します。 - 認可の決定: 検証が成功すると、
userid
とusername
が解析され、権限の判断またはデータのフィルタリングが行われます。
III. Next.js ミドルウェア:コアメカニズムと利点
3.1 ミドルウェアの種類
Next.js 13 以降では、次の 2 つのモードをサポートする新しいミドルウェアシステムが導入されています。
- グローバルミドルウェア: すべてのルートに有効です (
middleware-exclude
パターンに一致するルートを除く)。 - ルートミドルウェア: 特定のルートまたはルートグループにのみ有効です (ファイルの場所または構成を通じて定義)。
3.2 コア機能
- リクエストのインターセプト: リクエストがページまたは API ルートに到達する前に、リクエストヘッダーを変更し、Cookie を解析し、ID を検証します。
- レスポンスの制御: 認証結果に応じて、リダイレクトレスポンスを返します (たとえば、認証されていない場合はログインページにリダイレクトします)。
- エッジ実行: Vercel Edge Network での実行をサポートし、低レイテンシー認証を実現します (Node.js 固有の API の使用は避けてください)。
3.3 ミドルウェアのファイル構造
プロジェクトのルートディレクトリに middleware.ts
(TypeScript) または middleware.js
を作成し、Request
オブジェクトと NextRequest
オブジェクトを受信する非同期関数をエクスポートします。
import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export async function middleware(request: NextRequest) { // 認証ロジック const response = NextResponse.next(); return response; } // ミドルウェアのスコープを構成します (例:すべてのページルートを一致させます) export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'] };
IV. プロジェクトの設定:初期化から依存関係のインストールまで
4.1 Next.js プロジェクトを作成する
npx create-next-app@latest jwt-auth-demo # TypeScript、App Router (デフォルト)、ESLint (オプション) を選択します
4.2 依存関係をインストールする
npm install jsonwebtoken cookie @types/cookie # JWT 処理と Cookie の解析 npm install bcryptjs # パスワードのハッシュ化 (サーバー側のみで使用)
4.3 環境の構成
ルートディレクトリに .env.local
を作成して、JWT 署名キーと有効期限を保存します。
JWT_SECRET=your-strong-secret-key-128-bit-or-longer JWT_EXPIRES_IN=3600 # 1 時間 (秒単位)
V. ログイン機能の実装:JWT の生成と返却
5.1 ログイン API ルートを作成する
app/api/auth/login/route.ts
でログインロジックを実装します。
import { NextResponse } from 'next/server'; import jwt from 'jsonwebtoken'; import { compare } from 'bcryptjs'; import { User } from '@/types/user'; // カスタムユーザータイプ // モックデータベースユーザー (実際にはデータベースに接続する必要があります) const mockUsers: User[] = [ { id: '1', username: 'admin', password: '$2a$10$H6pXZpZ...' } // ハッシュ化されたパスワード ]; export async function POST(request: Request) { const { username, password } = await request.json(); // ユーザーを検索する const user = mockUsers.find(u => u.username === username); if (!user) { return NextResponse.json({ error: 'User does not exist' }, { status: 401 }); } // パスワードを検証する const isPasswordValid = await compare(password, user.password); if (!isPasswordValid) { return NextResponse.json({ error: 'Incorrect password' }, { status: 401 }); } // JWT を生成する const token = jwt.sign( { userId: user.id, username: user.username }, // ペイロードには userid と username が含まれます process.env.JWT_SECRET!, { expiresIn: process.env.JWT_EXPIRES_IN } ); // HttpOnly Cookie を設定する (セキュリティ属性) const response = NextResponse.json({ message: 'Login successful' }); response.cookies.set('authToken', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', // 本番環境では HTTPS を有効にする sameSite: 'lax', // CSRF 攻撃を防ぐ maxAge: Number(process.env.JWT_EXPIRES_IN), path: '/' }); return response; }
5.2 ログインページコンポーネント
app/login/page.tsx
でログインフォームを作成します。
'use client'; // クライアントコンポーネント import { useState } from'react'; import { useRouter } from 'next/navigation'; export default function LoginPage() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const router = useRouter(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); if (response.ok) { router.push('/dashboard'); // ログイン成功後にリダイレクトする } else { const data = await response.json(); console.error(data.error); } }; return ( <form onSubmit={handleSubmit}> <input type="text" placeholder="Username" value={username} onChange={(e) => setUsername(e.target.value)} required /> <input type="password" placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} required /> <button type="submit">Login</button> </form> ); }
VI. コアミドルウェアの実装:JWT の検証とユーザー情報の抽出
6.1 Cookie で JWT を解析する
まず、JWT の検証を処理するユーティリティ関数 utils/jwt.ts
を作成します。
import jwt from 'jsonwebtoken'; import { Cookie } from 'cookie'; export type JwtPayload = { userId: string; username: string; iat?: number; exp?: number; }; export function parseAuthCookie(cookieHeader: string | undefined): string | null { if (!cookieHeader) return null; const cookies = Cookie.parse(cookieHeader); return cookies.authToken || null; } export function verifyJwt(token: string): JwtPayload | null { try { return jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload; } catch (error) { console.error('JWT verification failed:', error); return null; } }
6.2 認証ミドルウェアを作成する
リクエストをインターセプトして JWT を検証するコアロジックを middleware.ts
で実装します。
import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; import { parseAuthCookie, verifyJwt } from './utils/jwt'; export async function middleware(request: NextRequest) { // 1. Cookie から JWT を取得する const token = parseAuthCookie(request.headers.get('cookie')); // 2. 保護されたルートを定義する (例:ログインページを除くすべてのページ) const isProtectedRoute = !request.nextUrl.pathname.startsWith('/login'); if (isProtectedRoute) { // 3. トークンが提供されていません:ログインページにリダイレクトする if (!token) { return NextResponse.redirect(new URL('/login', request.url)); } // 4. JWT を検証する const payload = verifyJwt(token); if (!payload) { // トークンが無効または期限切れの場合は、無効な Cookie をクリアします (オプション) const response = NextResponse.redirect(new URL('/login', request.url)); response.cookies.delete('authToken'); return response; } // 5. 検証に合格しました:ユーザー情報をリクエストコンテキストに添付します (オプション、ルートパラメーターと一緒に使用する必要があります) // または、後続の処理で getToken 関数を使用して取得します } else { // 6. ログインページ:認証されている場合は、ダッシュボードにリダイレクトする if (token && verifyJwt(token)) { return NextResponse.redirect(new URL('/dashboard', request.url)); } } // 7. リクエストの続行を許可する return NextResponse.next(); } // 8. ミドルウェアのスコープを構成します (API ルートと静的リソースを除くすべてのページを一致させます) export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'] };
6.3 ミドルウェアのコアロジックの分析
- ルート保護戦略:
/login
ページを除くisProtectedRoute
を介して認証が必要かどうかを判断します。 - トークンの存在チェック: 有効な Cookie が伝送されない場合は、ログインページにリダイレクトします。
- 署名の検証と有効期限のチェック:
jsonwebtoken
ライブラリを使用してトークンを検証し、有効期限 (exp
フィールド) を自動的に処理します。 - セキュリティの強化: トークンが無効な場合は、期限切れのトークンの再利用を避けるためにクライアントの Cookie をクリアします。
- ログインページの最適化: 認証されたユーザーがログインページにアクセスすると、ユーザーエクスペリエンスを向上させるために、ダッシュボードに自動的にリダイレクトします。
VII. ルートの保護:ページでユーザー情報を取得する
7.1 サーバーコンポーネントでユーザー情報を取得する
保護されたページ ( app/dashboard/page.tsx
など) で、JWT を再検証して userid
と username
を取得します。
import { NextResponse } from 'next/server'; import { parseAuthCookie, verifyJwt } from '@/utils/jwt'; export default async function Dashboard() { // リクエストヘッダーから Cookie を取得します (サーバーコンポーネントはリクエストオブジェクトにアクセスできます) const token = parseAuthCookie(headers.get('cookie')); const payload = token? verifyJwt(token) : null; if (!payload) { // 理論的には、ミドルウェアがインターセプトしていますが、エッジケースを処理する必要があります (リクエスト中にトークンが期限切れになるなど) return NextResponse.redirect(new URL('/login', request.url)); } return ( <div> <h1>ようこそ、{payload.username} さん!</h1> <p>ユーザー ID: {payload.userId}</p> </div> ); }
7.2 クライアントコンポーネントで認証ステータスを取得する
useEffect
またはサードパーティのライブラリ (SWR など) を介してサーバーからユーザーのステータスを取得します。
'use client'; import { useEffect, useState } from'react'; import { useRouter } from 'next/navigation'; export default function UserProfile() { const [user, setUser] = useState<{ userId: string; username: string } | null>(null); const router = useRouter(); useEffect(() => { // クライアントは API ルートにリクエストを送信して、トークンを検証し、ユーザー情報を返します const fetchUser = async () => { const response = await fetch('/api/auth/user'); if (response.ok) { const data = await response.json(); setUser(data); } else { router.push('/login'); } }; fetchUser(); }, [router]); return ( user? ( <div> <h2>ユーザー情報</h2> <p>ユーザー名: {user.username}</p> <p>ユーザー ID: {user.userId}</p> </div> ) : ( <p>読み込み中...</p> ) ); }
7.3 ユーザー情報 API ルートを作成する
app/api/auth/user/route.ts
でユーザー情報インターフェイスを提供します (ミドルウェアで保護する必要があります)。
import { NextResponse } from 'next/server'; import { parseAuthCookie, verifyJwt } from '@/utils/jwt'; export async function GET(request: Request) { const token = parseAuthCookie(request.headers.get('cookie')); const payload = token? verifyJwt(token) : null; if (!payload) { return NextResponse.json({ error: 'Unauthenticated' }, { status: 401 }); } return NextResponse.json({ userId: payload.userId, username: payload.username }); }
VIII. ログアウト:クライアントの Cookie をクリアする
8.1 ログアウト API ルートを実装する
app/api/auth/logout/route.ts
で authToken
Cookie をクリアします。
import { NextResponse } from 'next/server'; export async function POST(request: Request) { const response = NextResponse.redirect(new URL('/login', request.url)); response.cookies.delete('authToken', { path: '/' }); // Cookie をクリアします return response; }
8.2 ログアウトボタンコンポーネント
クライアントコンポーネントでログアウト API を呼び出します。
'use client'; import { useRouter } from 'next/navigation'; export default function LogoutButton() { const router = useRouter(); const handleLogout = async () => { await fetch('/api/auth/logout', { method: 'POST' }); router.push('/login'); }; return <button onClick={handleLogout}>ログアウト</button>; }
IX. セキュリティの強化:本番環境でのベストプラクティス
9.1 Cookie セキュリティ属性の構成
HttpOnly
: XSS 攻撃を防ぎます (トークンは JavaScript でアクセスできません)。Secure
: HTTPS 環境でのみ送信します (本番環境では必須)。SameSite: 'lax'
または'strict'
: CSRF 攻撃を防ぎます (lax
はほとんどのシナリオに適しており、strict
はより厳格です)。Domain
とPath
: Cookie のスコープを制限します (たとえば、yourdomain.com
のみがアクセスできるようにします)。
9.2 JWT の署名と有効期限の戦略
- 少なくとも 256 ビットのキー (
HS256
アルゴリズム) または RSA 非対称暗号化 (分散システムに適しています) を使用します。 - 短い有効期限 (1 時間など) を設定し、頻繁なログインを避けるためにリフレッシュトークンメカニズムと組み合わせます。
9.3 ミドルウェアのパフォーマンスの最適化
- エッジミドルウェア: 機密性の低い認証ロジック (トークンの存在チェックなど) をエッジノードに移動して、サーバーの負荷を軽減します。
- キャッシュ制御: 静的リソース (画像、CSS など) のミドルウェア処理をスキップします (
config.matcher
を介して除外されます)。
9.4 エラー処理とロギング
- ミドルウェアで JWT 解析エラーをキャッチし、詳細なログを記録します (本番環境では Sentry などの監視ツールを使用することをお勧めします)。
- 技術的な詳細を明らかにしないように、クライアントに一般的なエラーメッセージ (「認証に失敗しました」など) を表示します。
9.5 リフレッシュトークンメカニズム (拡張)
- ユーザーがログインすると、JWT とリフレッシュトークンの両方を返します (別の
HttpOnly Cookie
に保存されます)。 - JWT が期限切れになったら、リフレッシュトークンを使用して、再度ログインしなくてもサーバーから新しい JWT をリクエストします。
// リフレッシュトークン API の例 (app/api/auth/refresh/route.ts) export async function POST(request: Request) { const refreshToken = parseAuthCookie(request.headers.get('cookie')); // リフレッシュトークンを検証します (サーバー側のデータベースまたは Redis に保存する必要があります) // 新しい JWT を生成して返します }
X. よくある問題と解決策
10.1 ミドルウェアが機能しない?
middleware.ts
のファイルの場所を確認します (プロジェクトのルートディレクトリにある必要があります)。config.matcher
のマッチングルールが正しいことを確認します (絶対パスまたは正規表現を使用します)。
10.2 Cookie がリクエストで伝送されない?
- ログイン時に設定された Cookie の
path
が'/'
であることを確認します。 - 本番環境で
Secure: true
が有効になっている場合は、HTTPS 経由でアクセスする必要があります。
10.3 クライアントコンポーネントはどのようにユーザー情報を取得できますか?
- サーバー側の API インターフェイス (
/api/auth/user
など) を介して取得し、XSS を防ぐためにクライアントの Cookie を直接解析することは避けてください。
10.4 エッジミドルウェアでトークンの検証が失敗する?
- エッジ環境は Node.js ネイティブモジュールをサポートしていません。
jsonwebtoken
が純粋な JavaScript 実装 (jose
ライブラリの ES6 バージョンなど) を使用していることを確認します。
XI: セキュリティ強化対策
-
HttpOnly + Secure Cookie を使用する
-
妥当な SameSite ポリシーを設定する
-
JWT の有効期間は 2 時間を超えないようにする
-
暗号化キーを定期的にローテーションする
-
CSRF 保護を実装する
XII. 結論:セキュアで信頼性の高い認証システムの構築
Next.js ミドルウェアと JWT を組み合わせることで、完全な認証および認可システムを実装しました。これには、次のコアな利点があります。
- ステートレス認証: サーバーはセッションを保存する必要がなく、高い同時実行性とマイクロサービスアーキテクチャをサポートします。
- きめ細かいルート保護:
config.matcher
を介して保護する必要があるルートを柔軟に構成します。 - セキュリティの強化: Cookie セキュリティ属性とトークンの短い有効期限を合理的に使用して、攻撃対象領域を減らします。
- 優れたスケーラビリティ: ロールベースのアクセス制御 (RBAC) や多要素認証 (MFA) などの機能のその後の追加をサポートします。
実際のプロジェクトでは、ビジネス要件 (権限レベルの判断など) に応じてミドルウェアロジックを調整する必要があり、セキュリティのベストプラクティスを厳守する必要があります。Next.js のミドルウェアメカニズムは、認証プロセスを簡素化するだけでなく、フルスタックアプリケーションを構築するための統合されたリクエスト処理レイヤーも提供します。これは、現代のウェブ開発に不可欠なツールです。
この記事の実践を通して、開発者は JWT の生成、ミドルウェアの検証からユーザー情報の抽出までのプロセス全体を習得し、将来、より複雑な認証システムを開発するための強固な基盤を築くことができます。常に覚えておいてください。セキュリティは多層防御のシステムです。コードの実装に加えて、インフラストラクチャ (HTTPS など)、監視システム (異常なログインの検出など)、ユーザー教育 (強力なパスワードポリシーなど) と組み合わせて、真に信頼性の高いアプリケーションを構築する必要があります。
Leapcell: 最高のサーバーレス Web ホスティング
最後に、サービスのデプロイに最適なプラットフォームである Leapcell をお勧めします。
🚀 お気に入りの言語で構築
JavaScript、Python、Go、または Rust で簡単に開発できます。
🌍 無制限のプロジェクトを無料でデプロイ
使用量に応じて料金を支払うだけで、リクエストも料金もかかりません。
⚡ 従量課金制、隠れたコストなし
アイドル料金は不要で、シームレスなスケーラビリティを実現できます。
🔹 Twitter でフォローしてください: @LeapcellHQ