You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 10:37:00