第五章:ルート保護とフルスタックセキュリティ:Middleware設定とServer Component権限管理
前四章で、私たちはすべての基盤構築を完了しました。会員データは安全にSupabaseデータベースに格納され、セッション状態も常に把握できるようになりました。 いよいよSaaSプラットフォームの中核的価値である**「ペイウォール(Paywall)とルート保護(Route Protection)」**を実装する時が来ました。
もしシステムがルート保護を適切に実装していない場合、ユーザーがURLを推測する(例えば手動でhttps://vibe-tutor.com/courses/premium-video-1と入力する)だけで、高額なコースを無料で利用できてしまいます。
Next.js App Routerのアーキテクチャでは、業界で「二重絶対防御」と呼ばれるメカニズムを備えています:Edge MiddlewareによるインターセプトとServer Componentデータベースロックです。
この章では、この防護ネットを完璧に構築していきます。
🛡️ 第一層防御:Edge Middleware(入口の案内係)
あなたのウェブサイトを会員制高級クラブと想像してください。
Middlewareはクラブの入口に立つ案内係のようなものです。お客様(HTTPリクエスト)が「VIPエリア(/dashboardや/courses)」に入ろうとすると、案内係は瞬時にその胸の名札(Cookies)をスキャンします。
名札がない場合(未ログイン)、案内係は内部リソースを浪費させず、すぐに「登録カウンター(/login)」へと誘導します。
Middlewareの実装
Next.jsでは、Middlewareは非常に特殊なファイルです。プロジェクトのルートディレクトリ(またはsrcフォルダのルート)に配置する必要があり、ファイル名は必ずmiddleware.tsとしなければなりません。
src/middleware.tsを作成します:
import { auth } from "@/auth"
import { NextResponse } from "next/server"
// MiddlewareはVercel Edge Runtime(エッジコンピューティングノード)で動作するため、
// 非常に軽量で高速ですが、複雑なPostgreSQLデータベースに直接接続することはできません。
// ここでauth()関数はユーザーのCookieを復号化してログイン状態を判断します。
export default auth((req) => {
const isLoggedIn = !!req.auth;
// ユーザーがアクセスしようとしている完全なURLパスを取得
const { pathname } = req.nextUrl;
// 保護が必要なルートのプレフィックスを定義
const protectedRoutes = ["/dashboard", "/courses/premium", "/settings"];
// 現在のパスが保護対象かどうかを判定
const isProtectedPath = protectedRoutes.some(route => pathname.startsWith(route));
// インターセプトロジック:保護エリアにアクセスしようとしているが未ログインの場合
if (isProtectedPath && !isLoggedIn) {
// ログインページにリダイレクトするだけでなく、元々アクセスしようとしていたURL(callbackUrl)を記録
// これにより、ログイン成功後に自動的に元の動画ページに戻ることができます!
const encodedCallback = encodeURIComponent(pathname);
const redirectUrl = new URL(`/login?callbackUrl=${encodedCallback}`, req.url);
// 強制的に307リダイレクト
return NextResponse.redirect(redirectUrl);
}
// 追加ロジック:すでにログインしているユーザーが/loginにアクセスしようとした場合、ホームページにリダイレクト
if (pathname === "/login" && isLoggedIn) {
return NextResponse.redirect(new URL("/", req.url));
}
// 問題なければ次のステップへ進む
return NextResponse.next();
})
// 設定(オプティマイザ):Next.jsに静的なファイルをインターセプトしないように指示し、計算コストを節約
export const config = {
matcher: [
/*
* 以下のパスで始まるリクエストを除くすべてのリクエストパスにマッチ:
* - api (APIルート)
* - _next/static (静的ファイル)
* - _next/image (画像最適化ファイル)
* - favicon.ico (ファビコンファイル)
* - 公開画像ファイル(例:.png, .jpg)
*/
'/((?!api|_next/static|_next/image|favicon.ico|.*\\.png$).*)',
],
}
💡 Vibe Codingテストテクニック: 上記のコードを書いた後、シークレットウィンドウを開き、直接
http://localhost:3000/dashboardと入力してください。 画面がほとんど遅延なく、即座に/loginページにリダイレクトされることがわかります。これがEdgeエッジコンピューティングの究極のパフォーマンスで、サーバーを未承認のトラフィックから保護します。
🏰 第二層防御:Server Componentデータベースロック(内部の金庫)
Middlewareは高速ですが、致命的な弱点があります:「ログインしているかどうか」はわかりますが、「支払いをしているかどうか」はわかりません。
ハッカーが無料アカウントを登録し、通常の会員Cookieを取得すると、入口のMiddleware案内係を簡単に騙し、/courses/premiumに堂々と入ることができます。
このため、実際のレンダリング(Server Component)で第二の絶対防御を設ける必要があります。
有料コースページsrc/app/courses/premium/page.tsxを開きます:
import { auth } from "@/auth"
import { redirect } from "next/navigation"
import { createClient } from "@/utils/supabase/server"
// これはServer Componentなので、このコードは「サーバーサイド」で実行されます。
// ブラウザ上のハッカーはこのロジックを絶対に見ることができません!
export default async function PremiumCoursePage() {
// 1. ユーザーのセッションを取得
const session = await auth()
// Middlewareですでに一度ブロックしていますが、セキュリティのためバックエンドでもう一度検証(防御の深層化原則)
if (!session?.user) {
redirect("/login")
}
// 2. ユーザーIDを使用してデータベースから最も機密性の高い支払い状態を照会
const supabase = createClient()
const { data: userProfile, error } = await supabase
.from("users")
.select("membership_level, is_banned")
.eq("id", session.user.id)
.single()
// 3. 厳格なビジネスロジックによるインターセプト
if (error || !userProfile) {
throw new Error("会員データを取得できませんでした")
}
if (userProfile.is_banned) {
return <div className="text-red-500">アカウントは停止されています。カスタマーサポートにお問い合わせください。</div>
}
if (userProfile.membership_level !== 'VIP' && userProfile.membership_level !== 'Pro') {
// 顧客が支払っていない!直接決済ページに誘導
return (
<div className="flex flex-col items-center justify-center h-[50vh] space-y-4">
<h1 className="text-2xl font-bold">プレミアムビジネス実践コースのロックを解除</h1>
<p>これは1999円の高級コースです。現在の権限ではアクセスできません。</p>
<a
href="/pricing"
className="bg-primary text-white px-6 py-3 rounded-lg shadow-lg hover:scale-105 transition-transform"
>
VIPに今すぐアップグレード
</a>
</div>
)
}
// 4. すべての検証を通過!ついに高価なビジネス機密を表示できます
return (
<div className="p-8 max-w-4xl mx-auto">
<h1 className="text-4xl font-extrabold mb-4">👑 トップクラスのビジネスソースコード殿堂へようこそ</h1>
<p className="text-xl text-gray-600 mb-8">尊い {session.user.name}様、こんにちは!</p>
{/* ここにビデオプレーヤーや機密ソースコードのダウンロードリンクを配置 */}
<div className="aspect-video bg-black rounded-2xl flex items-center justify-center text-white/50 text-2xl shadow-2xl">
ここに厳重に保護された4Kチュートリアル動画を再生
</div>
<div className="mt-8 p-6 bg-yellow-50 border border-yellow-200 rounded-xl">
<h3 className="font-bold text-yellow-800">機密ソースコードダウンロード:</h3>
<a href="/api/download/source-code.zip" className="text-blue-600 underline">
3999円プラン専用ソースコードをダウンロード
</a>
</div>
</div>
)
}
💡 アーキテクチャの心得:なぜこのように二重にチェックする必要があるのか?
従来のSPA(Single Page Application、純粋なReact/Vueなど)アーキテクチャでは、よくある初歩的なミスがありました: **「フロントエンドでVIPかどうかを判断し、VIPならdisplay: block、そうでなければdisplay: noneにする」**という方法です。 この方法は、Chrome開発者ツールを少し知っている人なら、F12でCSSを変更するか、APIが返すJSONをインターセプトしてis_vip: trueに変更するだけで、簡単にペイウォールを突破できてしまいます。
しかし、App RouterのServer Componentアーキテクチャでは、ユーザーがサーバー上のif条件を通過しない限り、サーバーはビデオリンクやダウンロードリンクをHTMLにコンパイルしません!
ハッカーがF12を押し続け、ブラウザを壊しても、送信されていないデータを絶対に取得できません。これが「バックエンド絶対防御」と呼ばれるものです。
🎉 コース修了のまとめ
素晴らしい!この5つの章は非��に困難な旅でしたが、あなたは見事に乗り切りました! ここで、あなたが現在持っているハッカー級のスキルを振り返りましょう:
- OAuth 2.0原理の精通:トークン交換の根本的なロジックを理解し、セキュリティに関する面接質問にも恐れません。
- マルチプラットフォーム連携能力:GoogleやLineなどの大手テック企業の複雑な開発者ポータルを冷静に扱い、正しくCallback URLsを設定できます。
- データベースアーキテクチャ思考:不安定なCookieを堅牢なSupabase Database Sessionアーキテクチャにアップグレードしました。
- 企業レベルのセキュリティ防御:MiddlewareエッジインターセプトとServer Componentバックエンドロックの二重防御陣形を習得しました。
あなたが作成したログインシステムは、市場の80%の受託会社が作成するものよりも安全で高速かつ現代的です。 この知識を活用し、ユーザーが安心してお金を払えるSaaSプラットフォームをさらに構築してください!次の上級プロジェクトでお会いしましょう!