如何在NextJS Pages路由中用Supabase默认认证工具保护所有页面?
问题分析与修复方案
核心错误梳理
- SessionContextProvider使用顺序错误:
useSession/useUser必须放在Provider内部调用,否则无法获取有效会话数据 - useEffect用法违规:不能在useEffect中返回JSX元素,这违反React钩子规则
- 受保护路由逻辑位置错误:认证分支判断应该放在组件渲染流程中,而非副作用钩子
- 缺少initialSession传递:getStaticProps需要同步Supabase初始会话,否则客户端与服务端会话状态会不一致
修复后的代码实现
1. _app.tsx 完整修复
import { useState } from "react"; import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs' import { SessionContextProvider, useSession, Session } from '@supabase/auth-helpers-react' import { Auth, ThemeSupa } from '@supabase/auth-ui-react' import { AppProps } from 'next/app' function MyApp({ Component, pageProps }: AppProps<{ initialSession: Session }>) { const [supabase] = useState(() => createBrowserSupabaseClient()) // 内部组件封装路由保护逻辑,确保useSession在Provider内调用 const ProtectedRouteWrapper = () => { const session = useSession() // 受保护页面无会话时渲染登录UI,否则渲染目标页面 if (pageProps.protected && !session) { return <Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} theme="dark" providers={['email']} // 按需配置登录方式 /> } return <Component {...pageProps} /> } return ( <SessionContextProvider supabaseClient={supabase} initialSession={pageProps.initialSession} > <ProtectedRouteWrapper /> </SessionContextProvider> ) } export default MyApp
2. index.tsx 修复(受保护页面)
import Account from "@components/account"; import { useSession } from '@supabase/auth-helpers-react' import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs' const Home = () => { // 从hooks获取session,避免未定义变量错误 const session = useSession() return ( <div> <Account session={session} /> </div> ) } export async function getStaticProps() { // 获取初始会话并传递给_app.tsx,实现SSR会话同步 const supabase = createBrowserSupabaseClient() const { data: { session } } = await supabase.auth.getSession() return { props: { protected: true, initialSession: session, }, } } export default Home
3. Account组件优化(可选)
import { useState, useEffect } from 'react' import { useUser, useSupabaseClient, Session } from '@supabase/auth-helpers-react' import { Database } from '@utils/database.types' type Profiles = Database['public']['Tables']['profiles']['Row'] export default function Account({ session }: { session: Session }) { const supabase = useSupabaseClient<Database>() const user = useUser() const [loading, setLoading] = useState(true) const [username, setUsername] = useState<Profiles['username']>(null) useEffect(() => { // 增加存在性判断,避免无效请求 if (!user) return getProfile() }, [user]) async function getProfile() { try { setLoading(true) const { data, error, status } = await supabase .from('profiles') .select(`username`) .eq('id', user.id) .single() if (error && status !== 406) throw error if (data) setUsername(data.username) } catch (error) { alert('加载用户数据失败!') console.error(error) } finally { setLoading(false) } } async function updateProfile() { try { setLoading(true) if (!user) throw new Error('未找到用户') const updates = { id: user.id, username, updated_at: new Date().toISOString(), } const { error } = await supabase.from('profiles').upsert(updates) if (error) throw error alert('资料更新成功!') } catch (error) { alert('更新资料失败!') console.error(error) } finally { setLoading(false) } } return ( <div> <div> <label htmlFor="email">邮箱</label> <input id="email" type="text" value={session.user.email} disabled /> </div> <div> <label htmlFor="username">用户名</label> <input id="username" type="text" value={username || ''} onChange={(e) => setUsername(e.target.value)} /> </div> <div> <button onClick={updateProfile} disabled={loading || !username}> {loading ? '加载中...' : '更新'} </button> </div> <div> <button onClick={() => supabase.auth.signOut()}> 退出登录 </button> </div> </div> ) }
关键概念澄清
- Session:Supabase返回的会话对象,包含认证状态、token等核心信息,是判断用户是否登录的直接依据
- User:从Session中提取的用户基础信息,主要用于关联业务数据(如用户资料表)
- 统一路由保护逻辑:在_app.tsx中处理时,必须确保会话状态在SessionContextProvider内部获取,通过条件渲染控制登录UI与目标页面的切换
内容的提问来源于stack exchange,提问作者Jon
相关产品推荐
相关产品推荐

