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

Next.js 15 + Supabase SSR认证问题:重置密码路由无法防护

问题原因分析

  1. 会话状态未正确刷新:原middleware中缺少会话刷新步骤,supabase.auth.getUser()无法准确获取最新的未登录状态,导致跳过重定向逻辑。
  2. 路由保护逻辑不完整:仅限制了未登录用户,但未区分普通登录用户和通过密码重置流程登录的用户,不符合“仅能通过重置密码链接访问”的需求。
  3. 页面组件无双重验证:即使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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 05:34:57