Next.js未认证路由保护问题:阻止未登录用户直接访问路由
问题根源
你当前用useEffect做路由保护的核心问题是:useEffect是在组件客户端挂载完成后才执行,所以用户访问受保护路由时,页面会先渲染出来,再触发跳转,导致出现页面闪现的情况,无法真正拦截未授权访问。
以下是几种彻底解决的方案,按推荐优先级排序:
方案1:使用Next.js Middleware(推荐,Pages/App Router通用)
Middleware 是在请求到达页面之前执行的服务器端逻辑,能完全阻止未授权请求进入页面,不会有任何页面闪现。
在项目根目录创建middleware.ts(或.js)文件:
import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export function middleware(request: NextRequest) { // 从cookie获取登录状态(请根据你的实际存储方式调整,建议用httpOnly cookie存储认证信息) const authToken = request.cookies.get('auth-token')?.value; const selectedCustomerId = request.cookies.get('selected-customer')?.value; const isAuthenticated = !!authToken; const hasSelectedCustomer = !!selectedCustomerId; // 无需登录即可访问的白名单路由 const publicRoutes = ['/login', '/reset-password']; const isPublicRoute = publicRoutes.includes(request.nextUrl.pathname); // 未登录且访问非白名单路由 → 重定向到登录页 if (!isAuthenticated && !isPublicRoute) { return NextResponse.redirect(new URL('/login', request.url)); } // 已登录但未选择客户 → 重定向到客户选择页 if (isAuthenticated && !hasSelectedCustomer && request.nextUrl.pathname !== '/search-customer') { return NextResponse.redirect(new URL('/search-customer', request.url)); } // 已登录且已选客户,访问根目录 → 重定向到指定业务页面 if (isAuthenticated && hasSelectedCustomer && request.nextUrl.pathname === '/') { return NextResponse.redirect(new URL('/stock-items/categories', request.url)); } // 所有校验通过,允许请求继续 return NextResponse.next(); } // 指定中间件生效的路由范围(排除静态资源和API路由) export const config = { matcher: [ '/((?!_next/static|_next/image|favicon.ico|api).*)', ], };
方案2:Pages Router 下使用 getServerSideProps
如果你的项目用的是 Pages Router,可以在受保护页面中添加服务器端渲染逻辑,提前判断权限并重定向:
// 在需要保护的页面组件中添加 export async function getServerSideProps(context) { const { req, resolvedUrl } = context; // 从cookie读取登录状态(和Middleware逻辑保持一致) const authToken = req.cookies['auth-token']; const selectedCustomerId = req.cookies['selected-customer']; const isAuthenticated = !!authToken; const hasSelectedCustomer = !!selectedCustomerId; // 未登录且访问非白名单路由 → 重定向 if (!isAuthenticated && !['/login', '/reset-password'].includes(resolvedUrl)) { return { redirect: { destination: '/login', permanent: false, }, }; } // 已登录但未选客户 → 重定向 if (isAuthenticated && !hasSelectedCustomer && resolvedUrl !== '/search-customer') { return { redirect: { destination: '/search-customer', permanent: false, }, }; } // 已登录已选客户,访问根目录 → 重定向 if (isAuthenticated && hasSelectedCustomer && resolvedUrl === '/') { return { redirect: { destination: '/stock-items/categories', permanent: false, }, }; } return { props: {}, // 传递给页面组件的参数 }; }
可以把这个权限校验逻辑抽成公共函数,在所有受保护页面中复用。
方案3:改进客户端路由守卫(备选)
如果必须用客户端逻辑,可通过加载状态避免页面闪现,在权限校验完成前不渲染目标页面:
// 创建 AuthGuard 组件 import { useEffect, useState } from 'react'; import { useRouter } from 'next/router'; export default function AuthGuard({ children }) { const router = useRouter(); const [isAuthChecked, setIsAuthChecked] = useState(false); useEffect(() => { const validateAuth = async () => { // 替换成你的登录状态获取逻辑(比如从Context、localStorage或API) const loginUser = await fetchLoginUserInfo(); const publicRoutes = ['/login', '/reset-password']; const isPublicRoute = publicRoutes.includes(router.pathname); if (!loginUser.authenticated && !isPublicRoute) { router.push('/login'); } else if (loginUser.authenticated && !loginUser.selectedCustomer && router.pathname !== '/search-customer') { router.push('/search-customer'); } else if (loginUser.authenticated && loginUser.selectedCustomer && router.pathname === '/') { router.push('/stock-items/categories'); } else { setIsAuthChecked(true); } }; validateAuth(); }, [router]); // 校验未完成时显示加载态,不渲染子组件 if (!isAuthChecked) { return <div>加载中...</div>; // 可替换为项目统一的加载组件 } return <>{children}</>; }
然后在_app.js中用该组件包裹所有页面:
import AuthGuard from '../components/AuthGuard'; function MyApp({ Component, pageProps }) { return ( <AuthGuard> <Component {...pageProps} /> </AuthGuard> ); } export default MyApp;
注:该方案仍属于客户端逻辑,极端情况下可能存在极短的页面闪现,优先级低于前两种服务器端方案。
内容的提问来源于stack exchange,提问作者Prakash Narkhede
相关产品推荐
相关产品推荐

