React/Next中Cookie转LocalStorage及落地页自动登录问题解决
问题描述
我的项目包含4个源,已在用户源实现Cookie转LocalStorage且存储正常,但落地页源无法获取LocalStorage中的token,期望用户登录后落地页自动登录。请问如何在落地页渲染时完成Cookie转LocalStorage并实现自动登录?
落地页相关文件
main.js
import { useContext, createContext, useState, useEffect } from 'react' // import { setCookie } from 'cookies-next' import { useRouter } from 'next/router' import { cookies } from 'next/headers' import { useLogout } from '@/core/services/react-query/auth' const context = createContext() export const useMainContext = () => { return useContext(context) } const MainContextProvider = ({ children, theme: initialTheme, token: _token, }) => { const [theme, _setTheme] = useState(initialTheme) const [token, _setToken] = useState(_token) const [isOpen, _setIsOpen] = useState(false) const router = useRouter() const { pathname, asPath, query, locale } = router useEffect(() => { const storedTokenFromCookies = document.cookie .split('; ') .find(row => row.startsWith('token=')); if (storedTokenFromCookies) { const tokenValue = storedTokenFromCookies.split('=')[1]; setToken(tokenValue); // Set token in the state localStorage.setItem('token', tokenValue); // Also set it in localStorage console.log('User found in cookies:', tokenValue); // Redirect user to profile page router.push('/profile'); } else { const storedToken = localStorage.getItem('token'); if (storedToken) { setToken(storedToken); // Update state if the token exists in localStorage } } }, [router]); const toggleTheme = () => { const value = theme === 'dark' ? 'light' : 'dark' // setCookie('theme', value) localStorage.setItem('theme', value) _setTheme(value) if (value === 'dark') {document.documentElement.classList.add('dark') }else {document.documentElement.classList.remove('dark')} } const toggleLang = () => { const lang = locale === 'en' ? 'fa' : 'en' router.push({ pathname, query }, asPath, { locale: lang }) document.documentElement.setAttribute( 'dir', lang === 'en' ? 'ltr' : 'rtl' ) // setCookie('lang', lang, { path: '/' }) localStorage.setItem('lang', lang) } const setToken = (value) => { // setCookie('token', value) localStorage.setItem('token', value) _setToken(value) } const setIsOpen = () => { _setIsOpen((prevState) => !prevState) } return ( <context.Provider value={{ theme, toggleTheme, toggleLang, token, setToken, setIsOpen, isOpen, }}> {children} </context.Provider> ) } export default MainContextProvider
_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document' import { ServerStyleSheet } from 'styled-components'; class MyDocument extends Document { static async getInitialProps(ctx) { const sheet = new ServerStyleSheet() const originalRenderPage = ctx.renderPage try { // Run the React rendering logic synchronously ctx.renderPage = () => originalRenderPage({ // Useful for wrapping the whole react tree enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props} />), // Useful for wrapping in a per-page basis enhanceComponent: (Component) => Component, }) // Run the parent `getInitialProps`, it now includes the custom `renderPage` const initialProps = await Document.getInitialProps(ctx) const theme = ctx.req?.cookies?.theme || 'dark' const lang = ctx.req?.cookies?.lang const dir = lang === 'en' ? 'ltr' : 'rtl' return { ...initialProps, theme, dir, styles: [initialProps.styles, sheet.getStyleElement()] } } finally { sheet.seal() } } render() { return ( <Html className={this.props.theme} dir={this.props.dir}> <Head /> <body> <Main /> <NextScript /> </body> </Html> )}} export default MyDocument
_app.js
import '@/styles/globals.css' import Providers from '@/components/layout/Providers' import { getCookie, setCookie } from 'cookies-next' import MainLayout from '@/components/layout/main/MainLayout' import { GlobalStyles } from '@/styles/styled/GlobalStyle.styled' export default function App({ Component, pageProps, theme, ...rest }) { // TODO: if cookie is empty let _theme = theme if (!_theme) { _theme = 'dark' setCookie('theme', _theme) } return ( <Providers theme={_theme} {...rest}> <MainLayout pageProps={pageProps}> <GlobalStyles /> <Component {...pageProps} /> </MainLayout> </Providers> ) } App.getInitialProps = ({ ctx }) => { const theme = getCookie('theme', ctx) const token = getCookie('token', ctx) return { theme, token, } }
修复方案
1. 优化服务端到客户端的token同步逻辑
现有代码在main.js的useEffect中直接读取document.cookie,存在SSR时document未定义的风险,且没有利用_app.js中已经从服务端获取到的token。修改main.js的MainContextProvider:
const MainContextProvider = ({ children, theme: initialTheme, token: _token, }) => { const [theme, _setTheme] = useState(initialTheme) // 直接用服务端传来的token初始化状态 const [token, _setToken] = useState(_token) const router = useRouter() useEffect(() => { // 优先同步服务端的token到LocalStorage if (_token) { localStorage.setItem('token', _token); // 仅在当前不在profile页时跳转,避免循环 if (router.pathname !== '/profile') { router.push('/profile'); } } else { // 服务端未拿到token时,再读取客户端Cookie const storedTokenFromCookies = document.cookie .split('; ') .find(row => row.startsWith('token=')); if (storedTokenFromCookies) { const tokenValue = storedTokenFromCookies.split('=')[1]; setToken(tokenValue); localStorage.setItem('token', tokenValue); if (router.pathname !== '/profile') { router.push('/profile'); } } else { // 读取LocalStorage中已有的token const storedToken = localStorage.getItem('token'); if (storedToken) { setToken(storedToken); } } } }, [router, _token]); // 添加_token为依赖,确保服务端传值时触发同步 // 其余代码保持不变 }
2. 确保token正确传递到Context Provider
修改_app.js,将getInitialProps中获取的token明确传递给Providers(假设Providers内部包裹了MainContextProvider,需要透传token属性):
export default function App({ Component, pageProps, theme, token }) { let _theme = theme if (!_theme) { _theme = 'dark' setCookie('theme', _theme) } return ( // 明确传入token到Providers <Providers theme={_theme} token={token}> <MainLayout pageProps={pageProps}> <GlobalStyles /> <Component {...pageProps} /> </MainLayout> </Providers> ) } // getInitialProps代码保持不变
3. 跨源场景的额外处理
如果落地页和用户源属于不同域名,Cookie默认无法跨域共享,需要:
- 在用户源设置Cookie时,指定
domain为公共父域名(如两个域名都是.yourdomain.com),同时设置sameSite: 'Lax'(或None,需配合secure: true) - 若为完全独立的域名,可通过加密的URL参数传递token,或采用OAuth2授权码模式实现跨域登录
4. 验证逻辑有效性
- 落地页加载时,打开浏览器控制台,执行
localStorage.getItem('token')确认值存在 - 检查
MainContext中的token状态是否正确更新 - 确认自动跳转仅在未登录状态触发,避免无限循环
内容的提问来源于stack exchange,提问作者fgkh
相关产品推荐
相关产品推荐

