🐙
Next.jsとSupabaseで簡単にユーザー認証を実装する!
前提条件
- Next.jsのバージョン15.3.4
- Supabaseでユーザー登録をしておく
Supabaseの初期設定を行う
事前準備
New Organization
から新しく組織を作成
New project
から新しくプロジェクトを作成
手順
Authentication
を選択
Authenticaiton
内のSign in /Providers
を選択
Auth Providers
のEmailのEnabled
を選択
有効にしたいルールにチェックをつける
デフォルトでは以下の3つが有効になっている
- Enable Email Provider
- メール認証を有効にする
- Confirm email
- 有効にすればサインインの際にユーザーのメールアドレスで認証する
- Secure email change
- メール変更時の設定
Next.jsのコード
まずはsupabaseに必要なパッケージをインストール
npm install @supabase/supabase-js @supabase/ssr
ファイル構成は以下の通り
src/
├── middleware.ts # Next.js ミドルウェア
├── lib/
│ ├── supabaseMiddleware.ts # Supabase ミドルウェア関数
│ ├── supabase-server.ts # サーバーサイド Supabase クライアント
│ ├── supabase.ts # クライアントサイド Supabase クライアント
│ └── auth.ts # 認証ヘルパー関数
├── app/
│ ├── login/
│ │ └── page.tsx # ログインページ
│ └── page.tsx # ホームページ
└── components/
└── ... # 各種コンポーネント
まず、supabase/ssrではサーバーコンポーネントとクライアントコンポーネントで認証に必要なコードが異なる。
サーバーコンポーネント向けの認証
- ミドルウェアの実装
middleware.ts
import { type NextRequest } from 'next/server'
import { updateSession } from './lib/supabaseMiddleware'
export async function middleware(request: NextRequest) {
return await updateSession(request)
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
- サーバーサイド Supabase クライアントの実装
src/lib/supabase-server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createServerSupabaseClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// The `setAll` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
}
)
}
- Supabase ミドルウェア関数を実装
src/lib/supabaseMiddleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function updateSession(request: NextRequest) {
let supabaseResponse = NextResponse.next({
request,
})
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, }) => request.cookies.set(name, value))
supabaseResponse = NextResponse.next({
request,
})
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
)
},
},
}
)
const {
data: { user },
} = await supabase.auth.getUser()
if (
!user &&
!request.nextUrl.pathname.startsWith('/login') &&
!request.nextUrl.pathname.startsWith('/auth')
) {
const url = request.nextUrl.clone()
url.pathname = '/login'
return NextResponse.redirect(url)
}
return supabaseResponse
}
- 認証ヘルパー関数の実装
src/lib/auth.ts
import { createServerSupabaseClient } from "@/lib/supabase-server";
import { redirect } from "next/navigation";
export async function requireAuth() {
const supabase = await createServerSupabaseClient();
const { data: { user }, error } = await supabase.auth.getUser();
if (error || !user) {
console.log('認証失敗、ログインページにリダイレクト');
redirect('/login');
}
return user;
}
- サーバーコンポーネントで使う
src/app/page.tsx
import { requireAuth } from "@/lib/auth";
import Header from "@/components/Header";
export default async function HomePage() {
// 認証チェック
const user = await requireAuth();
console.log('ユーザー情報:', { user: user?.email });
return (
<>
<Header />
<main>
<h1>ようこそ、{user.email}さん</h1>
{/* ページコンテンツ */}
</main>
</>
);
}
クライアントコンポーネント向けの認証
- クライアントサイド Supabase クライアントの実装
src/lib/supabase.ts
import { createBrowserClient } from '@supabase/ssr'
// クライアントサイド用
export const createClient = () => {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
- クライアントコンポーネントでの使用例
src/app/login/page.tsx
"use client";
import { useState } from "react";
import { createClient } from "@/lib/supabase";
import { useRouter } from "next/navigation";
export default function LoginPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [message, setMessage] = useState("");
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
const handleLogin = async () => {
setIsLoading(true);
const supabase = createClient();
const { error } = await supabase.auth.signInWithPassword({
email,
password
});
if (error) {
setMessage(`ログインに失敗しました: ${error.message}`);
} else {
setMessage("ログインしました");
router.push("/");
}
setIsLoading(false);
};
return (
<div className="p-4">
<h2>ログイン</h2>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="メールアドレス"
className="border p-2 w-full mb-2"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="パスワード"
className="border p-2 w-full mb-2"
/>
<button
onClick={handleLogin}
disabled={isLoading}
className="bg-blue-600 text-white px-4 py-2 rounded w-full"
>
{isLoading ? "処理中..." : "ログイン"}
</button>
{message && <p className="mt-2">{message}</p>}
</div>
);
}
Supabaseの認証(サーバーvsクライアント)比較
サーバーサイド認証 | クライアントサイド認証 | |
---|---|---|
実行環境 | サーバー上 | ブラウザ |
コンポーネントの種類 | サーバーコンポーネント | クライアントコンポーネント |
セッション管理 | 読み取りのみ可能 | 読み取り、書き込み可能 |
データアクセス | 直接アクセス可能 | API経由でアクセス可能 |
サーバーサイド認証フロー
- ユーザーがページにアクセス
- ミドルウェアがリクエストを処理し、認証状態をチェック
- 未認証の場合はログインページにリダイレクト
- 認証済みの場合はサーバーコンポーネントが実行され、ユーザーデータを取得
- レンダリングされたHTMLがクライアントに送信される
クライアントサイド認証フロー
- ユーザーがページにアクセス
- 初期HTMLがロードされる
- Javascriptが実行され、クライアントコンポーネントがマウントされる
- クライアントコンポーネントがSupabaseクライアントを初期化
- 認証状態をチェックし、UIを更新
- ーザーがログインボタンをクリックすると、認証リクエストが送信される
- 認証成功後、UIが更新される
Supabase 認証の使い分け: 簡単な考え方
基本原則は以下の通り
- 読み取りはサーバーサイド
- データの取得や表示はサーバーコンポーネントで行う
- 例: ユーザープロフィールの表示、投稿一覧の取得
- 書き込みはクライアントサイド
- ユーザーの操作による変更はクライアントコンポーネントで行う
- 例: ログイン/ログアウト、フォーム送信、いいね機能
- 保護はミドルウェア
- ページの保護や認証チェックはミドルウェアで行う
- 例: 未ログインユーザーのリダイレクト
実際の例
やりたいこと | どちらを使うか | 理由 |
---|---|---|
ユーザーがログインしているか確認 | サーバーサイド | 初期表示に必要な情報だから |
ログインボタンを実装 | クライアントサイド | ユーザーの操作に応じて処理するから |
プロフィールデータを表示 | サーバーサイド | 読み取り操作だから |
プロフィール更新フォーム | クライアントサイド | フォーム送信という書き込み操作だから |
認証が必要なページを保護 | ミドルウェア | ページアクセス前に確認する必要があるから |
Discussion