如何防止浏览器的前进按钮绕过Next.js中间件?

前端开发 2026-07-07

我正在用Next.js构建一个用于旅行推荐的多标准决策网站。

流程如下:

  1. 用户在主页输入房间码(/)。
  2. 我校验房间码。
  3. 如果房间存在,用户将被重定向到/room
  4. /room时,用户输入邮箱并通过一次性验证码(OTP)进行验证。

我的要求是,/room 只有在用户先成功输入一个有效房间码后才能访问。

尝试1:会话存储

我把房间码存储在sessionStorage,并在允许访问/room之前进行检查。

这基本上可行,但如果用户:

  • 输入有效房间码,
  • 被重定向到/room
  • 按下浏览器的后退按钮,
  • 然后点击前进,

浏览器仍然返回到/room。此时,sessionStorage 已经为空,因此页面会加载出 roomCode = null

尝试2:中间件+ Cookie

随后我切换到使用Next.js中间件。

  • 输入有效房间码后,我创建了一个Cookie。
  • 中间件在允许访问/room之前会检查这个Cookie。
  • 当用户返回到主页时,我删除该Cookie。

然而,如果用户先按后退再按前进,浏览器仍然显示/room,即使Cookie已被删除且中间件应该阻止访问。

我的问题

  1. 为什么浏览器的前进按钮似乎绕过了Next.js中间件?
  2. 是否有可能在重定向到主页后清除浏览器历史记录,使用户无法返回或前往受保护的页面?
"use client";

import { useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";

import HeroImage from "@/components/heroimage/heroimage";
import Login from "@/components/emaillogin/emaillogin";
import OTPConfirm from "@/components/otp/otp";

export default function RoomPage() {
  const router = useRouter();

  const [step, setStep] = useState<"email" | "otp">(
    "email"
  );

  const [email, setEmail] = useState("");

  const historyLocked = useRef(false);

  const isDirty = email.trim() !== "";

  const confirmLeave = () => {
    if (!isDirty) return true;

    return window.confirm(
      "Your progress will be lost. Do you want to continue?"
    );
  };

  useEffect(() => {
    if (!isDirty || historyLocked.current) return;

    window.history.pushState(
      null,
      "",
      window.location.href
    );

    historyLocked.current = true;
  }, [isDirty]);

  useEffect(() => {
    const handleBeforeUnload = (
      e: BeforeUnloadEvent
    ) => {
      if (!isDirty) return;

      e.preventDefault();
      e.returnValue = "";
    };

    window.addEventListener(
      "beforeunload",
      handleBeforeUnload
    );

    return () => {
      window.removeEventListener(
        "beforeunload",
        handleBeforeUnload
      );
    };
  }, [isDirty]);

  useEffect(() => {
    const handlePopState = () => {
      if (!confirmLeave()) {
        window.history.pushState(
          null,
          "",
          window.location.href
        );
        return;
      }

      router.push("/");
    };

    window.addEventListener(
      "popstate",
      handlePopState
    );

    return () => {
      window.removeEventListener(
        "popstate",
        handlePopState
      );
    };
  }, [router, isDirty]);

  const handleBack = () => {
    if (step === "otp") {
      setStep("email");
      return;
    }

    if (!confirmLeave()) return;

    router.push("/");
  };

  return (
    <main className="relative min-h-screen">
      <div
        id="hero-section"
        className="absolute inset-0 -z-10"
      >
        <HeroImage />
      </div>

      <div className="relative z-10 mx-auto flex min-h-screen w-[80%] items-center justify-center">
        {step === "email" ? (
          <Login
            email={email}
            setEmail={setEmail}
            onSuccess={() => setStep("otp")}
            onBack={handleBack}
          />
        ) : (
          <OTPConfirm
            email={email}
            onBack={handleBack}
          />
        )}
      </div>
    </main>
  );
}

这是我的中间件

import { NextRequest, NextResponse } from "next/server";
import { jwtVerify } from "jose";

const getJwtSecretKey = () => {
  const secret = process.env.JWT_SECRET_KEY;

  if (!secret) {
    throw new Error("JWT_SECRET_KEY is not set.");
  }

  return new TextEncoder().encode(secret);
};

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const method = request.method;

  if (pathname === "/" && method === "GET") {
    const response = NextResponse.next();

    if (request.cookies.has("validateroom")) {
      response.cookies.delete("validateroom");
    }

    return response;
  }

  if (pathname === "/room") {
    const validateRoom = request.cookies.get("validateroom");

    if (!validateRoom) {
      return NextResponse.redirect(new URL("/", request.url));
    }

    return NextResponse.next();
  }

  const match = pathname.match(/^\/room\/([^/]+)\/.+$/);

  if (match) {
    const roomCodeFromUrl = match[1];

    const token = request.cookies.get("room_session")?.value;

    if (!token) {
      return NextResponse.redirect(new URL("/", request.url));
    }

    try {
      const { payload } = await jwtVerify(
        token,
        getJwtSecretKey()
      );

      if (payload.roomCode !== roomCodeFromUrl) {
        return NextResponse.redirect(new URL("/", request.url));
      }
    } catch {
      return NextResponse.redirect(new URL("/", request.url));
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/:path*"],
};

现在为了快速修复我使用了:

 useEffect(() => {
      router.refresh();
    }, [router]);

在我的Hero区域。它已经能完成工作,前进按钮仍然可用(没有变灰),但如果我尝试前进,我会立刻被推回到主页。

解决方案

为什么浏览器的前进按钮似乎绕过了Next.js中间件?

我不确定你完整的客户端路由设置,但很可能在底层使用了 push/pop-state(不经过服务器的客户端路由),因此你的后端根本不会收到一个http请求。你可以用浏览器调试器或服务器日志自行验证。

是否有可能在重定向到主页后清除浏览器历史记录,使用户无法返回或前往受保护的页面?

我建议不要采用试图修改浏览器历史记录来实现某种“特殊业务场景”的方案。相反,你可以在用户进入时设置一个会话值 lastRoomCode。页面重新加载时,roomCode 的值可以用会话中的 lastRoomCode 初始化。然后如果你的业务逻辑/用户操作是通过它完成的,只需从会话中移除此值。

如何阻止浏览器前进按钮绕过Next.js中间件?

也为了回答你在标题中的问题,你之所以会看到这种行为,是因为 react-router使用历史API(上面提到的push/pop-state)来进行导航。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章