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
相关产品推荐
相关产品推荐

