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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 00:25:26