Next.js 15 + Supabase SSR认证问题:重置密码路由无法防护
问题原因分析
- 会话状态未正确刷新:原middleware中缺少会话刷新步骤,
supabase.auth.getUser()无法准确获取最新的未登录状态,导致跳过重定向逻辑。 - 路由保护逻辑不完整:仅限制了未登录用户,但未区分普通登录用户和通过密码重置流程登录的用户,不符合“仅能通过重置密码链接访问”的需求。
- 页面组件无双重验证:即使middleware失效,页面组件未添加服务器端重定向,导致未登录用户能直接看到页面内容。
解决方案
1. 修复Middleware的会话刷新与路由保护
修改utils/supabase/middleware.ts,添加会话刷新逻辑,严格限制只有密码重置后的用户才能访问/update-password:
import { createServerClient } from '@supabase/ssr'; import { NextResponse, type NextRequest } from 'next/server'; export async function updateSession(request: NextRequest) { let supabaseResponse = NextResponse.next({ request }); const supabase = createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { getAll() { return request.cookies.getAll(); }, setAll(cookiesToSet) { cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value) ); supabaseResponse = NextResponse.next({ request }); cookiesToSet.forEach(({ name, value, options }) => supabaseResponse.cookies.set(name, value, options) ); }, }, } ); // 先刷新会话,确保用户状态最新 await supabase.auth.getSession(); const { data: { user } } = await supabase.auth.getUser(); // 未登录用户访问/update-password,重定向到登录页 if (!user && request.nextUrl.pathname.startsWith('/update-password')) { const url = request.nextUrl.clone(); url.pathname = '/login'; return NextResponse.redirect(url); } // 已登录但非密码重置会话的用户,重定向到首页 if (user && request.nextUrl.pathname.startsWith('/update-password')) { const { data: { user: userWithMetadata } } = await supabase.auth.getUser(); if (!userWithMetadata?.app_metadata?.password_reset) { const url = request.nextUrl.clone(); url.pathname = '/'; return NextResponse.redirect(url); } } return supabaseResponse; }
2. 在密码重置验证时添加会话标记
修改/auth/confirm/route.ts,验证token成功后给用户添加密码重置元数据标记:
import { type EmailOtpType } from '@supabase/supabase-js'; import { NextRequest, NextResponse } from 'next/server'; import { createClient } from '@/utils/supabase/server'; 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; const next = searchParams.get('next') ?? '/'; const baseURL = process.env.NEXT_PUBLIC_SITE_URL || request.nextUrl.origin; const redirectTo = new URL(next, baseURL); if (token_hash && type) { const supabase = await createClient(); const { error } = await supabase.auth.verifyOtp({ type, token_hash, }); if (!error) { // 添加密码重置标记到用户元数据 await supabase.auth.updateUser({ app_metadata: { password_reset: true } }); return NextResponse.redirect(redirectTo); } } return NextResponse.redirect(new URL('/error', baseURL)); }
3. 更新密码后移除标记
修改/update-password/actions.ts,成功更新密码后清除密码重置标记:
'use server'; import { createClient } from '@/utils/supabase/server'; export const updatePassword = async ( email: string, password: string ): Promise<{ message: string }> => { const supabase = await createClient(); try { const { error } = await supabase.auth.updateUser({ email, password, }); if (error) { console.error(error); return { message: 'Error updating password' }; } // 移除密码重置标记 await supabase.auth.updateUser({ app_metadata: { password_reset: null } }); return { message: 'Password updated successfully' }; } catch (error) { console.error(error); return { message: 'Error updating password' }; } };
4. 页面组件添加双重保护
修改/update-password/page.tsx,在服务器组件层面再次验证用户状态:
import { updatePassword } from './actions'; import { createClient } from '@/utils/supabase/server'; import { redirect } from 'next/navigation'; export default async function UpdatePassword() { const supabase = await createClient(); const { data } = await supabase.auth.getUser(); // 未登录用户直接重定向 if (!data.user) { redirect('/login'); } // 非密码重置会话的用户重定向到首页 if (!data.user.app_metadata?.password_reset) { redirect('/'); } const handleSubmit = async (formData: FormData) => { 'use server'; const password = formData.get('password'); const { message } = await updatePassword( data.user.email, password as string ); console.log(message); }; return ( <form action={handleSubmit}> <input type="password" name="password" required /> <button type="submit">Update password</button> </form> ); }
内容的提问来源于stack exchange,提问作者Detla888
相关产品推荐
相关产品推荐

