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

Supabase Email OTP登录异常:会话未创建致无法跳转Dashboard

NextJS + Supabase Email OTP 登录后无法跳转至仪表盘问题

问题现象

使用NextJS和Supabase开发时,Email OTP功能出现异常:输入邮箱能正常收到OTP验证码,但点击登录后无法跳转到/dashboard页面,反而回到登录页。

调试日志

GET /login 200 in 5ms
 ✓ Compiled in 42ms (237 modules)
Middleware triggered for path: /auth/callback
Middleware session check: Not found 
Allowing auth callback to proceed
 ✓ Compiled /auth/callback in 245ms (1646 modules)
 ⚠ Fast Refresh had to perform a full reload. Read more: https://nextjs.org/docs/messages/fast-refresh-reload
Auth callback triggered
Auth code received: No
No code or session, redirecting to login
 GET /auth/callback 307 in 334ms
Middleware triggered for path: /login
Middleware session check: Not found 
 GET /login 200 in 93ms

从日志可见,会话未被正确创建,导致中间件检测不到会话,最终重定向回登录页。

相关代码

middleware.ts

import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export async function middleware(req: NextRequest) {
  const res = NextResponse.next()
  const supabase = createMiddlewareClient({ req, res })

  try {
    const { data: { session }, error } = await supabase.auth.getSession()
    if (error) {
      console.error('Error fetching session:', error)
    }

    // Debug logs
    console.log('Middleware path:', req.nextUrl.pathname)
    console.log('Session status:', session ? 'Found' : 'Not found')

    if (!session && req.nextUrl.pathname.startsWith('/dashboard')) {
      console.log('No session found, redirecting to /login')
      return NextResponse.redirect(new URL('/login', req.url))
    }

    if (session && req.nextUrl.pathname === '/login') {
      console.log('Session exists, redirecting to /dashboard')
      return NextResponse.redirect(new URL('/dashboard', req.url))
    }

    return res
  } catch (err) {
    console.error('Middleware error:', err)
    return NextResponse.redirect(new URL('/login', req.url))
  }
}

export const config = {
  matcher: ['/dashboard/:path*', '/login', '/auth/callback']
}

auth/callback/route.ts

import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'

export async function GET(request: Request) {
  console.log('Auth callback triggered')
  const requestUrl = new URL(request.url)
  const code = requestUrl.searchParams.get('code')
  
  console.log('Auth code received:', code ? 'Yes' : 'No')

  if (code) {
    const supabase = createRouteHandlerClient({ cookies })
    console.log('Exchanging code for session...')
    
    try {
      const { data, error } = await supabase.auth.exchangeCodeForSession(code)
      
      if (error) {
        console.error('Auth error during exchange:', error)
        return NextResponse.redirect(new URL('/login', request.url))
      }

      if (data?.session) {
        // Explicitly set the session
        await supabase.auth.setSession(data.session)
        console.log('Session set successfully, redirecting to dashboard')
        return NextResponse.redirect(new URL('/dashboard', request.url))
      }
    } catch (error) {
      console.error('Unexpected error during auth:', error)
      return NextResponse.redirect(new URL('/login', request.url))
    }
  }

  console.log('No code or session, redirecting to login')
  return NextResponse.redirect(new URL('/login', request.url))
}

登录组件中的handleVerifyCode()函数

const handleVerifyCode = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    console.log('Starting OTP verification...');

    try {
      const { data, error } = await supabase.auth.verifyOtp({
        email,
        token: code,
        type: 'email',
      });
      console.log('OTP verification response:', { data, error });

      if (error) throw error;

      if (data?.session) {
        console.log('Session verified, redirecting to dashboard');
        router.push('/dashboard');
      } else {
        throw new Error('No session returned from OTP verification');
      }
    } catch (error: any) {
      console.error('Error verifying code:', error.message);
      alert(error.message);
    } finally {
      setLoading(false);
    }
  };

问题分析与解决方案

核心问题

  1. 验证流程混淆:当前代码混用了客户端直接验证(verifyOtp)和授权码回调(exchangeCodeForSession)两种模式,但Email OTP的verifyOtp调用无需走回调路由,日志中/auth/callback收到无code参数的无效请求,导致会话未被正确持久化。
  2. 会话同步延迟:客户端验证成功后,中间件可能因Cookie未及时同步,无法检测到最新会话。

修复步骤

1. 调整handleVerifyCode函数,确保会话持久化

调用verifyOtp后显式刷新会话,保证Cookie同步到浏览器:

const handleVerifyCode = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    console.log('Starting OTP verification...');

    try {
      const { data, error } = await supabase.auth.verifyOtp({
        email,
        token: code,
        type: 'email',
      });
      console.log('OTP verification response:', { data, error });

      if (error) throw error;

      // 显式刷新会话,确保Cookie同步
      const { data: { session: refreshedSession } } = await supabase.auth.getSession();
      
      if (refreshedSession) {
        console.log('Session verified, redirecting to dashboard');
        // 使用replace避免浏览器后退回到登录页
        router.replace('/dashboard');
      } else {
        throw new Error('No session returned from OTP verification');
      }
    } catch (error: any) {
      console.error('Error verifying code:', error.message);
      alert(error.message);
    } finally {
      setLoading(false);
    }
  };

2. 优化中间件,处理会话刷新逻辑

检测不到会话时尝试主动刷新,避免Cookie未同步导致的误判:

export async function middleware(req: NextRequest) {
  const res = NextResponse.next()
  const supabase = createMiddlewareClient({ req, res })

  try {
    let { data: { session }, error } = await supabase.auth.getSession()
    
    // 无会话时尝试刷新
    if (!session) {
      const { data: { session: refreshedSession }, error: refreshError } = await supabase.auth.refreshSession()
      if (refreshError) {
        console.error('Error refreshing session:', refreshError)
      } else {
        session = refreshedSession
      }
    }

    // Debug logs
    console.log('Middleware path:', req.nextUrl.pathname)
    console.log('Session status:', session ? 'Found' : 'Not found')

    if (!session && req.nextUrl.pathname.startsWith('/dashboard')) {
      console.log('No session found, redirecting to /login')
      return NextResponse.redirect(new URL('/login', req.url))
    }

    if (session && req.nextUrl.pathname === '/login') {
      console.log('Session exists, redirecting to /dashboard')
      return NextResponse.redirect(new URL('/dashboard', req.url))
    }

    return res
  } catch (err) {
    console.error('Middleware error:', err)
    return NextResponse.redirect(new URL('/login', req.url))
  }
}

3. 检查Supabase配置

确认Supabase控制台Authentication > URL Configuration中的Site URL和Redirect URLs已正确设置为你的应用域名(如http://localhost:3000),避免跨域导致Cookie无法设置。

4. 移除不必要的回调触发

Email OTP验证无需跳转回调页,若发送OTP代码中设置了redirectTo参数,需移除:

// 发送OTP的代码示例(如有)
await supabase.auth.signInWithOtp({
  email: userEmail,
  // 移除redirectTo参数
  // redirectTo: `${window.location.origin}/auth/callback`,
})

验证修复

完成修改后重新测试:

  1. 输入邮箱接收OTP
  2. 输入OTP点击登录,查看控制台是否显示Session verified, redirecting to dashboard
  3. 确认成功跳转至/dashboard,且中间件日志显示Session status: Found

内容的提问来源于stack exchange,提问作者yvirdi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 13:24:55