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

Next.js中如何结合嵌套布局实现路由保护与动态布局切换?

Next.js 路由保护与动态布局切换问题解决方案

核心问题分析

  1. localStorage未定义错误:Next.js根布局会在服务端渲染,服务端环境不存在浏览器的localStorage对象,直接访问会抛出异常。
  2. 布局切换需手动刷新:登录成功跳转后,布局组件的认证状态未实时同步,导致组件无法自动切换到对应布局,需刷新页面触发状态重新获取。

解决方案步骤

1. 修复localStorage访问问题(区分客户端/服务端)

React组件中,仅能在客户端环境访问localStorage,可通过useEffect(仅在客户端执行)或环境判断来安全获取token:

修改你的RootLayout组件:

import dynamic from "next/dynamic";
import type { ReactNode } from "react";
import { useState, useEffect } from "react";

const Applayout = dynamic(() => import("../Applayout"));
const Authlayout = dynamic(() => import("../Authlayout"));

export default function Rootlayout({ children }: { children: ReactNode }) {
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  useEffect(() => {
    // 仅在客户端执行,读取localStorage中的token
    const token = localStorage.getItem("Bearer");
    setIsAuthenticated(!!token);
  }, []);

  const LayoutComponent = isAuthenticated ? Applayout : Authlayout;
  return <LayoutComponent>{children}</LayoutComponent>;
}

2. 全局认证状态同步(解决跳转后布局不更新)

上述方案仅在组件初始化时读取token,登录后跳转不会触发状态更新,需通过React Context实现全局状态同步:

步骤1:创建Auth全局上下文

// contexts/AuthContext.tsx
import { createContext, useContext, useState, useEffect, ReactNode } from "react";

interface AuthContextType {
  isAuthenticated: boolean;
  login: (token: string) => void;
  logout: () => void;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  useEffect(() => {
    // 初始化时读取本地token
    const token = localStorage.getItem("Bearer");
    setIsAuthenticated(!!token);
  }, []);

  const login = (token: string) => {
    localStorage.setItem("Bearer", token);
    setIsAuthenticated(true);
  };

  const logout = () => {
    localStorage.removeItem("Bearer");
    setIsAuthenticated(false);
  };

  return (
    <AuthContext.Provider value={{ isAuthenticated, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error("useAuth must be used within an AuthProvider");
  }
  return context;
}

步骤2:在根组件中包裹AuthProvider

如果使用App Router,修改app/layout.tsx:

import { AuthProvider } from "@/contexts/AuthContext";
import Rootlayout from "./Rootlayout";

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        <AuthProvider>
          <Rootlayout>{children}</Rootlayout>
        </AuthProvider>
      </body>
    </html>
  );
}

步骤3:修改RootLayout使用全局状态

import dynamic from "next/dynamic";
import type { ReactNode } from "react";
import { useAuth } from "@/contexts/AuthContext";

const Applayout = dynamic(() => import("../Applayout"));
const Authlayout = dynamic(() => import("../Authlayout"));

export default function Rootlayout({ children }: { children: ReactNode }) {
  const { isAuthenticated } = useAuth();
  const LayoutComponent = isAuthenticated ? Applayout : Authlayout;
  return <LayoutComponent>{children}</LayoutComponent>;
}

步骤4:更新登录Hook的状态同步逻辑

// 修改useSubmitForm Hook
import { useAuth } from "@/contexts/AuthContext";

export const useSubmitForm = ({ type, authData }: Props) => {
  const [isLoading, setIsLoading] = useState(false);
  const [errorMessage, setErrorMessage] = useState("");
  const router = useRouter();
  const { login } = useAuth(); // 引入全局登录方法

  const { mutateAsync: loginMutate } = useLogin();
  const { mutateAsync: registerMutate } = useRegister();

  const submitHandler = async () => {
    setIsLoading(true);
    setErrorMessage("");

    try {
      let response;
      let token;

      if (type === formType.LOGIN) {
        response = await loginMutate(authData);
        token = response.data.token;
        toast("Successfully logged in!", { type: "success" });
      } else {
        response = await registerMutate(authData);
        token = response.data.token;
        toast("Successfully registered!", { type: "success" });
      }

      // 使用全局方法更新认证状态
      login(token);
      router.prefetch("/dash");
      setTimeout(() => {
        setIsLoading(false);
        router.push("/dash");
      }, 2002);
    } catch (error: any) {
      const message = error?.response?.data || "An error occurred";
      setErrorMessage(message);
      setIsLoading(false);
      toast(message, { type: "error" });
    }
  };

  return { isLoading, errorMessage, submitHandler };
};

3. 实现路由保护(无token时限制访问)

方案1:客户端路由守卫(配合Auth Context)

在Applayout中添加守卫逻辑:

// Applayout.tsx
import { useEffect } from "react";
import { useRouter } from "next/router";
import { useAuth } from "@/contexts/AuthContext";

export default function Applayout({ children }: { children: ReactNode }) {
  const { isAuthenticated } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (!isAuthenticated) {
      router.push("/auth/login");
    }
  }, [isAuthenticated, router]);

  // 未认证时显示加载或空状态
  if (!isAuthenticated) return null;

  return (
    <div className="app-layout-container">
      {/* 你的App布局内容 */}
      {children}
    </div>
  );
}

在Authlayout中添加反向守卫:

// Authlayout.tsx
import { useEffect } from "react";
import { useRouter } from "next/router";
import { useAuth } from "@/contexts/AuthContext";

export default function Authlayout({ children }: { children: ReactNode }) {
  const { isAuthenticated } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (isAuthenticated) {
      router.push("/dash");
    }
  }, [isAuthenticated, router]);

  // 已认证时显示加载或空状态
  if (isAuthenticated) return null;

  return (
    <div className="auth-layout-container">
      {/* 你的Auth布局内容 */}
      {children}
    </div>
  );
}

方案2:服务端中间件(后续改Cookie后使用)

当你将JWT改为HttpOnly Cookie后,可使用Next.js中间件实现更可靠的服务端路由保护,创建app/middleware.ts:

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  const token = request.cookies.get("Bearer")?.value;
  const path = request.nextUrl.pathname;
  const isAuthPath = path.startsWith("/auth");

  // 无token且不在认证路径,重定向到登录页
  if (!token && !isAuthPath) {
    return NextResponse.redirect(new URL("/auth/login", request.nextUrl));
  }

  // 有token且在认证路径,重定向到后台首页
  if (token && isAuthPath) {
    return NextResponse.redirect(new URL("/dash", request.nextUrl));
  }

  return NextResponse.next();
}

// 指定中间件生效的路径
export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

后续优化建议

  1. 优先切换到HttpOnly Cookie存储JWT:localStorage存在XSS攻击风险,HttpOnly Cookie无法被前端JS读取,安全性更高,且支持服务端中间件直接读取。
  2. 添加JWT有效性校验:当前仅判断token是否存在,后续需在服务端或客户端校验token的签名、过期时间等,避免无效token绕过保护。
  3. 添加加载状态组件:在布局切换、路由跳转过程中显示加载动画,提升用户体验。

内容的提问来源于stack exchange,提问作者Nedim

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 01:42:22