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); } };
问题分析与解决方案
核心问题
- 验证流程混淆:当前代码混用了客户端直接验证(
verifyOtp)和授权码回调(exchangeCodeForSession)两种模式,但Email OTP的verifyOtp调用无需走回调路由,日志中/auth/callback收到无code参数的无效请求,导致会话未被正确持久化。 - 会话同步延迟:客户端验证成功后,中间件可能因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`, })
验证修复
完成修改后重新测试:
- 输入邮箱接收OTP
- 输入OTP点击登录,查看控制台是否显示
Session verified, redirecting to dashboard - 确认成功跳转至
/dashboard,且中间件日志显示Session status: Found
内容的提问来源于stack exchange,提问作者yvirdi
相关产品推荐
相关产品推荐

