Next.js中使用localStorage持久化登录状态失效问题排查
登录状态无法持久化(Next.js v12.2.5 + React v18.2.0)
核心问题
基于Next.js和React开发的认证应用,浏览器刷新后登录会话失效。已通过localStorage存储JWT token,但问题依旧。期望登录状态在刷新后保留,仅主动登出时终止。
应用流程
用户通过登录表单调用Spring接口从MySQL获取凭证,登录、条件渲染及路由保护功能正常。登录成功后JWT token已成功存入localStorage(Chrome控制台可查),但刷新后状态丢失。
背景调研
Stack Overflow相关问题多聚焦Cookie方案,提及localStorage的仅建议用useEffect包裹获取逻辑,未解决问题。考虑过useReducer方案,但当前用useEffect实现,可接受重构。怀疑Next.js的SSR特性是问题根源。
解决方案
问题根源分析
- 初始化逻辑顺序错误:
useState(initialToken)在组件挂载时执行,而initialToken的赋值在useEffect中(组件挂载后才触发),导致初始state始终为空,刷新后无法从localStorage恢复token。 - 服务端localStorage访问错误:Next.js SSR阶段
localStorage不存在,代码中else分支仍尝试获取token,会触发报错。 - admin状态未持久化:admin状态仅存在内存中,未存入localStorage,刷新后直接丢失。
- 角色判断逻辑冗余:循环字符串判断角色的写法低效且无意义,可直接用
includes判断。
修复步骤
- 修正token初始化逻辑:直接在
useState中从localStorage取值(需判断window是否存在,避免SSR报错),移除无效的useEffect。 - 持久化admin状态:将admin标识存入localStorage,初始化时同步读取。
- 优化路由保护逻辑:改用Next.js的
useRouter获取当前路径,避免直接操作window.location。 - 简化角色判断逻辑:直接从返回数据中提取权限信息判断admin身份。
修改后的代码
auth-context.js
import React, { useState } from 'react'; const AuthContext = React.createContext({ token: '', isAdmin: false, isLoggedIn: false, login: (token, isAdmin) => {}, logout: () => {}, }); export const AuthContextProvider = (props) => { // 初始化时直接从localStorage取值,兼容SSR环境 const initialToken = typeof window !== 'undefined' ? localStorage.getItem('token') : null; const initialIsAdmin = typeof window !== 'undefined' ? localStorage.getItem('isAdmin') === 'true' : false; const [token, setToken] = useState(initialToken); const [isAdmin, setIsAdmin] = useState(initialIsAdmin); const userIsLoggedIn = !!token; const loginHandler = (token, isAdmin) => { setToken(token); setIsAdmin(isAdmin); localStorage.setItem('token', token); localStorage.setItem('isAdmin', isAdmin.toString()); }; const logoutHandler = () => { setToken(null); setIsAdmin(false); localStorage.removeItem('token'); localStorage.removeItem('isAdmin'); }; const contextValue = { token, isAdmin, isLoggedIn: userIsLoggedIn, login: loginHandler, logout: logoutHandler, }; return ( <AuthContext.Provider value={contextValue}> {props.children} </AuthContext.Provider> ); }; export default AuthContext;
ProtectRoute.js
import { useContext } from 'react'; import { useRouter } from 'next/router'; import AuthContext from './auth-context'; import HomePage from './HomePage'; import RestrictedSection from './RestrictedSection'; const ProtectRoute = ({ children }) => { const authCtx = useContext(AuthContext); const router = useRouter(); const currentPath = router.pathname; if (!authCtx.isLoggedIn) { if (currentPath === '/') { return <HomePage />; } else if (currentPath !== '/auth') { return <RestrictedSection />; } } return children; }; export default ProtectRoute;
AuthForm.js
import { useRef, useState, useContext } from 'react'; import { useRouter } from 'next/router'; import AuthContext from './auth-context'; import classes from './AuthForm.module.css'; const API_LOGIN_URL = '你的登录接口地址'; const AuthForm = () => { const emailInputRef = useRef(); const passwordInputRef = useRef(); const [isLoading, setIsLoading] = useState(false); const router = useRouter(); const authCtx = useContext(AuthContext); const submitHandler = (event) => { event.preventDefault(); setIsLoading(true); const enteredEmail = emailInputRef.current.value; const enteredPassword = passwordInputRef.current.value; const myHeaders = new Headers(); myHeaders.append("Content-Type", "application/x-www-form-urlencoded"); const urlencoded = new URLSearchParams(); urlencoded.append("username", enteredEmail); urlencoded.append("password", enteredPassword); const requestOptions = { method: 'POST', headers: myHeaders, body: urlencoded, redirect: 'follow' }; fetch(API_LOGIN_URL, requestOptions) .then(async (res) => { setIsLoading(false); if (res.ok) { return res.json(); } else { const data = await res.json(); throw new Error('Authentication failed!'); } }) .then((data) => { // 直接从返回数据中判断admin角色 const isAdmin = data.authorities?.includes('ROLE_SUPER_ADMIN'); authCtx.login(data.access_token, isAdmin); router.replace('/'); }) .catch((err) => { alert(err.message); setIsLoading(false); }); }; return ( <section className={classes.auth}> <h1>Login</h1> <form onSubmit={submitHandler}> <div className={classes.control}> <label htmlFor='email'>Your Email</label> <input type='email' id='email' required ref={emailInputRef} /> </div> <div className={classes.control}> <label htmlFor='password'>Your Password</label> <input type='password' id='password' required ref={passwordInputRef} /> </div> <div className={classes.actions}> {!isLoading && <button>Login</button>} {isLoading && <p>Sending request</p>} </div> </form> </section> ); }; export default AuthForm;
内容的提问来源于stack exchange,提问作者jay tai
相关产品推荐
相关产品推荐

