NextJS 13路由控制:仅允许通过应用内按钮跳转路由
你的核心问题是前端路由跳转与后端数据库更新的时序不匹配:线上环境数据库操作存在延迟,导致Middleware校验时还没读到最新的页面状态,直接把跳转请求重定向回去了。下面给出几种可落地的解决方案,按优先级排序:
方案一:等待数据库更新完成后再跳转(最直接有效)
问题根源是你调用异步的useSetCurrentPage后立刻执行router.push,没等数据库更新完成。只需修改按钮逻辑,确保后端确认页面更新后再触发路由跳转。
修改按钮点击逻辑
// 原错误代码(未等待异步操作完成) // handlenextPage{ useSetCurrentPage("/battle/step2"); router.push("/battle/step2")} // 修改后的正确逻辑 const handleNextPage = async () => { await useSetCurrentPage("/battle/step2"); // 等待数据库更新成功 router.push("/battle/step2"); };
为什么有效
线上环境的数据库操作(尤其是跨区域部署或有读写分离的场景)会有网络延迟,await会强制前端等待后端返回更新成功的响应,此时Middleware查询数据库时能拿到最新的页面状态,校验自然通过。
方案二:优化Middleware,应对数据库延迟
如果方案一仍存在极端场景的延迟(比如主从同步延迟),可以给Middleware增加重试机制,或者强制读取主库数据。
2.1 增加数据库查询重试
给getCurrentPageInDb加最多2次重试,每次间隔100ms,覆盖短暂的同步延迟:
import { getCurrentPageInDb } from "./services/currentPage/currentPage.service"; import { NextRequest, NextResponse } from "next/server"; import { parseCookie } from "utils"; const MAX_RETRIES = 2; const RETRY_DELAY = 100; // 延迟100ms再重试 // 带重试的页面查询函数 const getCurrentPageWithRetry = async (accessToken: string, retries: number): Promise<string | null> => { try { return await getCurrentPageInDb(accessToken); } catch (err) { if (retries > 0) { await new Promise(resolve => setTimeout(resolve, RETRY_DELAY)); return getCurrentPageWithRetry(accessToken, retries - 1); } throw err; } }; export default async function middleware(req: NextRequest) { const cookieHeader = req.headers.get("cookie"); if (!cookieHeader) return NextResponse.redirect(new URL("/", req.url)); const cookies = parseCookie(cookieHeader); const accessToken = cookies["access-token"]; if (!accessToken) return NextResponse.redirect(new URL("/", req.url)); let currentPage; try { currentPage = await getCurrentPageWithRetry(accessToken, MAX_RETRIES); } catch (err) { console.error("获取当前页面失败:", err); return NextResponse.redirect(new URL("/", req.url)); } if (currentPage && req.nextUrl.pathname !== currentPage) { return NextResponse.redirect(new URL(currentPage, req.url)); } return NextResponse.next(); } export const config = { matcher: ["/battle/:path*", "/admin/:path*"], };
2.2 强制读取主库(针对读写分离场景)
如果后端用了主从数据库,默认查询可能走从库,导致更新后无法立即读到最新数据。修改NestJS的getCurrentPageInDb服务,强制查询主库:
// NestJS服务示例(以TypeORM为例) async getCurrentPageInDb(accessToken: string): Promise<string | null> { // 指定使用主库连接查询 const user = await this.userRepository.query( 'SELECT current_page FROM users WHERE access_token = $1', [accessToken], { connection: 'master' } // 替换为你的主库连接名 ); return user?.[0]?.current_page || null; }
方案三:前端状态配合后端校验(双重保障)
在前端维护当前页的全局状态,先拦截非法跳转,同时保留Middleware作为最终校验层,减少后端请求压力。
3.1 创建全局页面状态Context
// contexts/CurrentPageContext.tsx 'use client'; import { createContext, useContext, useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { useSetCurrentPage } from '@/hooks/useSetCurrentPage'; const CurrentPageContext = createContext<{ currentPage: string | null; navigateToNextPage: (page: string) => Promise<void>; }>({ currentPage: null, navigateToNextPage: async () => {}, }); export const CurrentPageProvider = ({ children }: { children: React.ReactNode }) => { const [currentPage, setCurrentPageState] = useState<string | null>(null); const router = useRouter(); const updatePage = useSetCurrentPage; // 页面加载时从后端获取初始页面 useEffect(() => { const fetchInitialPage = async () => { const accessToken = document.cookie.split('; ').find(row => row.startsWith('access-token'))?.split('=')[1]; if (!accessToken) return; const res = await fetch('/api/current-page', { headers: { 'Authorization': `Bearer ${accessToken}` } }); const data = await res.json(); setCurrentPageState(data.currentPage); }; fetchInitialPage(); }, []); // 统一处理跳转逻辑:更新数据库 → 更新前端状态 → 跳转路由 const navigateToNextPage = async (page: string) => { await updatePage(page); setCurrentPageState(page); router.push(page); }; return ( <CurrentPageContext.Provider value={{ currentPage, navigateToNextPage }}> {children} </CurrentPageContext.Provider> ); }; export const useCurrentPage = () => useContext(CurrentPageContext);
3.2 在页面组件中使用Context
'use client'; import { useCurrentPage } from '@/contexts/CurrentPageContext'; const Step1Page = () => { const { navigateToNextPage } = useCurrentPage(); const handleNext = () => { navigateToNextPage("/battle/step2"); }; return ( <button onClick={handleNext}>进入第二步</button> ); }; export default Step1Page;
3.3 前端拦截非法路由(可选)
用beforePopState拦截浏览器前进后退或手动修改URL的行为:
// 在CurrentPageProvider的useEffect中添加 useEffect(() => { const unsubscribe = router.beforePopState(({ url }) => { const targetPath = new URL(url, window.location.origin).pathname; if (currentPage && targetPath !== currentPage) { router.push(currentPage); return false; // 阻止默认跳转 } return true; }); return () => unsubscribe(); }, [currentPage, router]);
方案四:Cookie同步页面状态(减少数据库依赖)
除了数据库存储,同时将当前页写入Cookie,Middleware优先校验Cookie,再同步数据库状态,避免频繁查询数据库。
4.1 修改前端页面更新逻辑
export const useSetCurrentPage = async (currentPageToken: string) => { const accessToken = document.cookie.split('; ').find(row => row.startsWith('access-token'))?.split('=')[1]; if (!accessToken) { console.error('未找到access-token'); return; } // 先设置Cookie,立即生效 document.cookie = `current-page=${currentPageToken}; path=/; secure; same-site=strict`; // 再更新数据库 await updateCurrentPageInDb(accessToken, currentPageToken); };
4.2 修改Middleware校验逻辑
export default async function middleware(req: NextRequest) { const cookieHeader = req.headers.get("cookie"); if (!cookieHeader) return NextResponse.redirect(new URL("/", req.url)); const cookies = parseCookie(cookieHeader); const accessToken = cookies["access-token"]; const currentPageFromCookie = cookies["current-page"]; if (!accessToken) return NextResponse.redirect(new URL("/", req.url)); // 优先用Cookie校验,避免数据库查询延迟 if (currentPageFromCookie && req.nextUrl.pathname !== currentPageFromCookie) { return NextResponse.redirect(new URL(currentPageFromCookie, req.url)); } // Cookie不存在时,查询数据库并同步Cookie const currentPage = await getCurrentPageInDb(accessToken); if (currentPage) { const response = NextResponse.next(); response.cookies.set("current-page", currentPage, { path: "/", secure: true, sameSite: "strict" }); if (req.nextUrl.pathname !== currentPage) { return NextResponse.redirect(new URL(currentPage, req.url)); } return response; } return NextResponse.next(); }
注意:Cookie可能被用户篡改,所以关键操作(比如提交表单)仍需校验数据库中的真实页面状态,避免安全风险。
内容的提问来源于stack exchange,提问作者Botnari Ion

