Next.js 项目 Supabase Auth 集成指南
基于 Supabase 官方 Next.js 认证指南 和 Nexty.dev 模板实际代码,本文档将详细介绍 Supabase Auth 在 Next.js 项目中的代码集成步骤和使用方式。
集成步骤详解
步骤 1:安装依赖包
npm install @supabase/supabase-js @supabase/ssr
依赖包说明:
@supabase/supabase-js
- Supabase JavaScript 客户端库@supabase/ssr
- 专为服务端渲染优化的辅助库
步骤 2:环境变量配置
创建 .env.local
文件并配置必要的环境变量:
# Supabase 基础配置
NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key
NEXT_PUBLIC_GOOGLE_CLIENT_ID=your_google_client_id
环境变量详细说明请查看:环境变量
步骤 3:创建 Supabase 客户端
3.1 浏览器客户端
lib/supabase/client.ts
import { Database } from '@/lib/supabase/types'
import { createBrowserClient } from '@supabase/ssr'
export const createClient = () => {
return createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
这是专用于浏览器环境的客户端,用于:
- 处理用户交互(登录、注册、数据更新)
- 监听实时订阅数据变化
- 提供认证状态管理。
3.2 服务端客户端
lib/supabase/server.ts
import { Database } from '@/lib/supabase/types'
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient<Database>(
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 {
// Server Component 中调用 setAll 会被忽略
}
},
},
}
)
}
这是专用于服务端组件的客户端,用于:
- Server Actions 中的数据库操作
- Route Handlers 中的 API 处理
- 通过 Cookies 管理用户会话
步骤 4:设置中间件认证
我们需要先基于 Supabase 的功能完成中间件所需的权限控制逻辑。
lib/supabase/middleware.ts
/*
* 此处展示核心逻辑,非文件完整代码
*/
// 需要管理员权限的路由
const ROLE_PROTECTED_ROUTES: Record<string, string[]> = {
'dashboard/users': ['admin'],
};
// 需要登录的路由
const PROTECTED_ROUTES = ['dashboard'];
export async function updateSession(request: NextRequest) {
// 获取用户信息
const { data: { user } } = await supabase.auth.getUser()
// 检查登录状态
if (!user && isProtectedRoute) {
return NextResponse.redirect(new URL("/login", request.url))
}
// 检查角色权限
if (user && needsRoleCheck) {
const { data } = await supabase.from("users").select("role").eq("id", user.id).single();
if (!allowedRoles.includes(data?.role || "user")) {
return NextResponse.redirect(new URL("/403", request.url))
}
}
}
这个文件提供了实现认证状态管理逻辑:
- 通过
updateSession()
检查和更新用户认证状态 - 处理 token 刷新、会话验证等
还提供了权限控制的逻辑:
- 管理员权限的页面禁止其他用户访问
- 已登录普通用户的页面禁止未登录用户访问
- 重定向到登录页设置回调地址,用户登录成功后可直接进入目标页面
将 lib/supabase/middleware.ts
引入 middleware.ts
,使上述逻辑生效。
middleware.ts
import { updateSession } from '@/lib/supabase/middleware';
import createIntlMiddleware from 'next-intl/middleware';
import { NextRequest, NextResponse } from 'next/server';
import { routing } from './i18n/routing';
const intlMiddleware = createIntlMiddleware(routing);
export async function middleware(request: NextRequest): Promise<NextResponse> {
// 1. 先处理 Supabase 认证
const supabaseResponse = await updateSession(request);
if (supabaseResponse.headers.get('location')) {
return supabaseResponse;
}
// 2. 再处理国际化路由
const intlResponse = intlMiddleware(request);
// 3. 合并 Cookie
supabaseResponse.cookies.getAll().forEach((cookie) => {
const { name, value, ...options } = cookie;
intlResponse.cookies.set(name, value, options);
});
return intlResponse;
}
这样我们就完成了中间件的完整需求:
- 自动刷新过期的认证令牌
- 权限检查,保护需要登录的路由
- 角色验证,检查管理员权限
- 国际化集成
步骤 5:前端认证状态管理
在 [locale]/layout.tsx
文件中,我们引入 <AuthProvider>
实现了前端全局认证状态管理。
5.1 AuthProvider 的核心功能
AuthProvider.tsx
type AuthContextType = {
user: ExtendedUser | null; // 带角色信息的用户对象
loading: boolean; // 加载状态
signInWithGoogle: () => Promise<{ error: AuthError | null }>;
signInWithGithub: () => Promise<{ error: AuthError | null }>;
signInWithEmail: (email: string, captchaToken?: string) => Promise<{ error: AuthError | null }>;
signOut: () => Promise<void>;
refreshUser: () => Promise<void>; // 手动刷新用户信息
};
5.2 扩展用户信息
模板对 Supabase 的用户类型进行了扩展,添加了角色信息:
type ExtendedUser = User & {
role: "admin" | "user";
};
const fetchUserRole = async (userId: string) => {
const { data, error } = await supabase
.from("users")
.select("role")
.eq("id", userId)
.limit(1)
.maybeSingle();
return data?.role || "user";
};
如果你的前端还需要 User
表或其他数据表的重要用户数据,可以在 fetchUserRole
方法里扩展。
5.3 实时角色更新
模板实现了角色变化的实时监听:
const userSubscription = supabase
.channel("public:users")
.on(
"postgres_changes",
{
event: "UPDATE",
schema: "public",
table: "users",
filter: `id=eq.${user?.id}`,
},
async (payload) => {
if (user) {
setUser({
...user,
role: payload.new.role,
});
}
}
)
.subscribe();
5.4 多种登录方式支持
社交登录:
const signInWithGoogle = async () => {
return await supabase.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: `${window.location.origin}/auth/callback?next=${next}`,
},
});
};
const signInWithGithub = async () => {
return await supabase.auth.signInWithOAuth({
provider: "github",
options: {
redirectTo: `${window.location.origin}/auth/callback?next=${next}`,
},
});
};
邮箱 Magic Link:
const signInWithEmail = async (email: string, captchaToken?: string) => {
return await supabase.auth.signInWithOtp({
email: normalizeEmail(email),
options: {
emailRedirectTo: `${window.location.origin}/auth/callback?next=${next}`,
captchaToken,
},
});
};
步骤 6:认证回调处理
模板实现了完整的认证回调处理机制。
6.1 OAuth 回调
app/auth/callback/route.ts
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url)
const code = searchParams.get('code')
let next = searchParams.get('next') ?? '/'
if (code) {
const supabase = await createClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
return NextResponse.redirect(`${origin}${next}`)
}
}
return NextResponse.redirect(new URL(`/redirect-error?code=server_error`, origin))
}
6.2 邮箱确认
app/auth/confirm/route.ts
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const token_hash = searchParams.get('token_hash')
const type = searchParams.get('type') as EmailOtpType | null
if (!token_hash || !type) {
return NextResponse.redirect(new URL(`/redirect-error?code=invalid_params`, origin))
}
const supabase = await createClient()
const { error } = await supabase.auth.verifyOtp({
type,
token_hash,
})
if (error) {
// 详细的错误处理
switch (error.message) {
case 'Token has expired':
return NextResponse.redirect(new URL(`/redirect-error?code=token_expired`, origin))
// ... 其他错误处理
}
}
return NextResponse.redirect(new URL(next, origin))
}
实际项目中的应用
在客户端组件中使用
import { useAuth } from '@/components/providers/AuthProvider';
export default function UserProfile() {
const { user, loading, signOut } = useAuth();
if (loading) return <div>Loading...</div>;
if (!user) {
return <LoginButton />;
}
return (
<div>
<h1>Welcome, {user.email}</h1>
<p>Role: {user.role}</p>
{user.role === 'admin' && <AdminPanel />}
<button onClick={signOut}>Sign Out</button>
</div>
);
}
在服务端组件中使用
import { createClient } from '@/lib/supabase/server';
import { redirect } from 'next/navigation';
export default async function PrivatePage() {
const supabase = await createClient();
const { data: { user }, error } = await supabase.auth.getUser();
if (error || !user) {
redirect('/login');
}
return <p>Hello {user.email}</p>;
}
在 Server Actions 中使用
'use server'
import { createClient as createAdminClient } from "@supabase/supabase-js";
import { isAdmin } from '@/lib/supabase/isAdmin';
export async function adminAction() {
if (!(await isAdmin())) {
return actionResponse.forbidden("Admin privileges required.");
}
const supabaseAdmin = createAdminClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
// 执行管理员操作
}
安全最佳实践
1. 服务端使用 getUser()
而非 getSession()
// ✅ 正确 - 服务端总是验证令牌
const { data: { user } } = await supabase.auth.getUser();
// ❌ 错误 - 服务端不应使用,令牌可能未验证
const { data: { session } } = await supabase.auth.getSession();
2. 分离客户端和服务端逻辑
// 客户端组件 - 使用浏览器客户端
const supabase = createClient(); // from @/lib/supabase/client
// 服务端组件/Actions - 使用服务端客户端
const supabase = await createClient(); // from @/lib/supabase/server
3. 敏感操作鉴权后使用 Service Role Key
涉及敏感操作,需要先判断是管理员身份,然后使用 Service Role Key 操作数据库
import { createClient as createAdminClient } from "@supabase/supabase-js";
import { isAdmin } from '@/lib/supabase/isAdmin';
// 判断管理员身份
if (!(await isAdmin())) {
return actionResponse.forbidden("Admin privileges required.");
}
// 管理员使用 Service Role Key 操作数据库
const supabaseAdmin = createAdminClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY! // 仅服务端使用
);