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

Next.js客户端页面鉴权:useContext()与localStorage对比

Next.js 基于 useContext 的页面保护方案及问题解决

方案概述

在Next.js中,常用useContext结合包装器实现页面保护:将鉴权信息(如用户token、登录状态)存储在全局的AuthContext中,通过AuthProvider包裹_app.js里的所有页面组件,再用ProtectedRoute组件作为包装器,对需要鉴权的页面进行拦截。

完整代码示例

1. 创建AuthContext

// contexts/AuthContext.js
import { createContext, useContext, useState, useEffect } from 'react';

const AuthContext = createContext();

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // 只能在useEffect中读取localStorage,避免SSR错误
    const savedToken = localStorage.getItem('authToken');
    if (savedToken) {
      // 可在此处调用接口验证token有效性
      setUser({ token: savedToken });
    }
  }, []);

  // 登录、登出方法
  const login = (token) => {
    localStorage.setItem('authToken', token);
    setUser({ token });
  };

  const logout = () => {
    localStorage.removeItem('authToken');
    setUser(null);
  };

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

export const useAuth = () => useContext(AuthContext);

2. 在_app.js中使用AuthProvider

// pages/_app.js
import { AuthProvider } from '../contexts/AuthContext';

function MyApp({ Component, pageProps }) {
  return (
    <AuthProvider>
      <Component {...pageProps} />
    </AuthProvider>
  );
}

export default MyApp;

3. 实现ProtectedRoute组件

// components/ProtectedRoute.js
import { useAuth } from '../contexts/AuthContext';
import { useRouter } from 'next/router';

export function ProtectedRoute({ children }) {
  const { user } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (!user) {
      router.push('/login');
    }
  }, [user, router]);

  if (!user) return null;

  return children;
}

遇到的问题

  • 刷新页面的错误重定向:已登录用户刷新页面时,AuthContext中的useEffect执行顺序晚于受保护页面的useEffect,导致页面先检测到user为null,触发重定向,等AuthContext加载完token后已经来不及。
  • SSR错误与首次渲染无token:直接在useState中读取localStorage会引发服务端渲染错误(服务端无localStorage对象),只能在useEffect中获取token,导致首次渲染时AuthContext中无鉴权信息,页面会短暂显示未登录状态,影响体验。
  • 临时方案的隐患:直接在受保护页面读取localStorage判断登录状态,可能引发hydration不匹配错误(服务端渲染的内容和客户端首次渲染内容不一致),需要额外添加状态判断规避。

技术问题解答

1. 使用useContext()进行页面保护/鉴权的核心意义是什么?

  • 全局状态共享:鉴权状态(登录状态、用户信息)在所有组件中可直接获取,无需通过props层层传递,避免"props drilling"问题。
  • 逻辑集中维护:登录、登出、状态更新等鉴权逻辑统一封装在Context中,所有页面复用同一套逻辑,后续修改鉴权规则时只需改动Context,无需逐个修改页面。
  • 组件解耦:页面组件无需关心鉴权的具体实现细节(比如token存在localStorage还是cookie),只需依赖Context提供的状态和方法,降低组件间的耦合度。

2. 使用useContext()时,如何避免已登录用户刷新页面就被重定向的问题?

方法1:给AuthContext添加加载状态

在AuthContext中新增isLoading状态,标记鉴权信息是否加载完成。在ProtectedRoute中先等待加载完成,再判断是否允许访问,避免提前触发重定向。

修改后的AuthContext:

// contexts/AuthContext.js
export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true); // 新增加载状态

  useEffect(() => {
    const savedToken = localStorage.getItem('authToken');
    if (savedToken) {
      // 可选:验证token有效性
      setUser({ token: savedToken });
    }
    setIsLoading(false); // 加载完成后更新状态
  }, []);

  // ... 登录、登出方法不变

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

修改后的ProtectedRoute:

// components/ProtectedRoute.js
export function ProtectedRoute({ children }) {
  const { user, isLoading } = useAuth();
  const router = useRouter();

  useEffect(() => {
    // 加载完成后再判断是否重定向
    if (!isLoading && !user) {
      router.push('/login');
    }
  }, [user, isLoading, router]);

  // 加载中时显示占位内容(如骨架屏、加载动画)
  if (isLoading) {
    return <div>加载中...</div>;
  }

  if (!user) return null;

  return children;
}

方法2:结合服务端渲染方法(getServerSideProps/getStaticProps)

对于需要服务端渲染的页面,直接在服务端获取用户鉴权信息(如从cookie中读取token),提前判断登录状态,避免客户端渲染的顺序问题。

示例(getServerSideProps):

// pages/protected-page.js
import { ProtectedRoute } from '../components/ProtectedRoute';

export async function getServerSideProps(context) {
  // 从请求cookie中获取token(假设token存在cookie里)
  const token = context.req.cookies.authToken;
  
  if (!token) {
    return {
      redirect: {
        destination: '/login',
        permanent: false,
      },
    };
  }

  // 可选:调用接口验证token有效性
  const isValid = await verifyToken(token);
  if (!isValid) {
    return { redirect: { destination: '/login' } };
  }

  return { props: {} };
}

export default function ProtectedPage() {
  return (
    <ProtectedRoute>
      {/* 页面内容 */}
      <h1>受保护页面</h1>
    </ProtectedRoute>
  );
}

方法3:避免在页面useEffect中自行判断登录状态

将重定向逻辑完全封装在ProtectedRoute组件中,页面组件只负责渲染内容,不要在页面的useEffect中做登录状态判断,从根源上避免执行顺序问题。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 15:15:05