Laravel+React登录后刷新浏览器跳转至登录页问题求助
Laravel + React 登录刷新后重定向至登录页问题
成功登录后刷新浏览器,页面被重定向到登录页,但确认token未过期,怀疑路由配置或前端状态持久化存在问题。相关代码如下:
RequireAuth.jsx
import { useEffect } from 'react'; import useAuth from '../hooks/useAuth'; const RequireAuth = ({ allowedRoles }) => { const { auth } = useAuth(); const location = useLocation(); return ( auth?.role_id ? ( allowedRoles.some(role => role === auth.role_id) ? <Outlet /> : auth.user ? <Navigate to="/unauthorized" state={{from: location }} replace /> : <Navigate to="/login" state={{ from: location }} replace /> ) : <Navigate to="/login" state={{from: location }} replace /> ); } export default RequireAuth;
Login.jsx
import React from 'react'; import * as MUI from '../../import'; import axios from '../../api/axios'; import BGIMG from '../../assets/SchoolBG1.jpg'; import useAuth from '../../hooks/useAuth'; import useLoginStore from '../Store/LoginStore' import { useForm, Controller } from 'react-hook-form'; import { useState, useEffect } from "react"; import { Link, useNavigate, useLocation} from 'react-router-dom'; import { DevTool } from '@hookform/devtools'; const defaultTheme = MUI.createTheme(); const EMAIL_REGEX = /^(([^<>()[\]\.,;:\s@"]+(\.[^<>()[\]\.,;:\s@"]+)*)|(".+"))@(([^<>()[[\]\.,;:\s@"]+\.)+[^<>()[\]\.,;:\s@"]{2,})$/i; const LOGIN_URL = '/api/login' export default function Login() { const { setAuth } = useAuth(); const navigate = useNavigate(); const location = useLocation(); const from = location.state?.from?.pathname || "/"; const [loggedIn, setLoggedIn] = useState(); const { errMsg, setErrMsg, loading, setLoading, showPassword, expirationTime, setExpirationTime, handleTogglePassword, token, setToken, } = useLoginStore(); const form = useForm(); const { register, handleSubmit, control, formState } = form; const { errors } = formState; //Accepted Domains for the email const isSupportedDomain = (email) => { const supportedDomains = ['yahoo.com', 'gmail.com', 'outlook.com']; const educationalDomainRegex = /@[a-zA-Z0-9.-]+\.(edu(\.[a-zA-Z]{2})?|[a-zA-Z]{2}\.[a-zA-Z]{2})$/; const domain = email.split('@')[1]; return supportedDomains.includes(domain) || educationalDomainRegex.test(email); }; const csrf = () => axios.get("/sanctum/csrf-cookie") const onSubmit = async (data, event) => { event.preventDefault(); // Prevent default form submission behavior setLoading(true); await csrf() try { const config = { headers: { "Content-type": "application/json", } }; const postData = { email_address: data.email_address, password: data.password, }; const response = await axios.post( LOGIN_URL, JSON.stringify(postData), config, ); const { remember_token, user, roles_name, expires_at } = response.data; setLoggedIn(remember_token) console.log(JSON.stringify(response?.data)); const role_id = response?.data?.user?.role_id; setExpirationTime(new Date(expires_at).getTime()); setLoading(false); // Set the authenticated user state using setAuth from your useAuth hook setAuth({ user, remember_token, roles_name, role_id, expirationTime}); // Set the token and expiration time in local storage localStorage.setItem('remember_token', remember_token); const rolePath = role_id === 1 ? '/' : role_id === 2 ? '/' : role_id === 3 ? '/scholar-dashboard' : '/login'; // Navigate the user to the intended route (from variable contains the intended route) navigate(rolePath); } catch (err) { setLoading(false); if(err.response?.status === 422){ setErrMsg('Email address and password are required'); } else if(err.response?.status === 409){ setErrMsg('Email Address already been taken'); } else if(err.response?.status === 500){ setErrMsg('Server Error'); } else if (err.response?.status === 401) { setErrMsg('Login failed. Please check your credentials and try again.'); } else{ setErrMsg('Network error occurred. Please try again.'); } } } {/* When its size to a Tablet mode the Left Grid display none */} const isMobile = MUI.useMediaQuery('(max-width:768px)'); return ( <MUI.ThemeProvider theme={defaultTheme}> {/* Right Grid is the Picture Grid */} <MUI.Grid container component="main" sx={{ height: '100vh' }}> <MUI.CssBaseline /> {!isMobile && ( <MUI.Grid container alignItems='center' justifyContent='center' item xs={false} sm={12} md={7} sx={{ display: 'flex', // Use flex container flexDirection: 'column', // Stack items vertically alignItems: 'center', // Center items horizontally justifyContent: 'center', // Center items vertically backgroundImage: `url(${BGIMG})`, backgroundRepeat: 'no-repeat', backgroundColor: (t) => t.palette.mode === 'light' ? t.palette.grey[50] : t.palette.grey[900], backgroundSize: 'cover', backgroundPosition: 'center', textAlign: 'center', padding: '2rem', // Add padding to the content }} > <br /> <MUI.Typography variant="h2" color="background.paper" paragraph> Welcome to Gado and Jess <br/> Jalandoni Scholarship Program </MUI.Typography> <MUI.Typography variant="h5" color="background.paper" paragraph> Empowering dreams through education. </MUI.Typography> </MUI.Grid> )} {/* Left Grid is the Login Form */} <MUI.Grid item xs={12} sm={12} md={5} component={MUI.Paper} elevation={6} square container alignItems='center' justifyContent='center' sx={{ p: 3, }}> {loading && ( <MUI.Backdrop open={loading} sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }} > <MUI.Box sx={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', zIndex: (theme) => theme.zIndex.drawer + 2, }} > <MUI.CircularProgress /> </MUI.Box> </MUI.Backdrop> )} {errMsg && ( <p id="errMsg" style={{ color: 'red' }}> {' '} <MUI.InfoIcon className="infoErr" /> {errMsg} </p> )} <MUI.Box sx={{ my: 1, mx: 4, display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '100%', }}> <img src='https://raw.githubusercontent.com/TianMeds/image--stocks-for-coding/main/Scholarlink%20Logos.png' alt="Logo" style={{ width: '200px', height: '150px' }} /> <MUI.Typography component="h3" variant="h4" sx={{ textAlign: 'left', fontWeight: 'bold' }}> Login </MUI.Typography> <MUI.Typography> Welcome back! Please enter your details. </MUI.Typography> <br/> {/* Email Textfield */} <MUI.Box component="form" sx={{ mt: 1, width: '100%', }} onSubmit={(event) => handleSubmit((data) => onSubmit(data, event))(event)} method='post' > <MUI.Grid item> <MUI.TextField variant='standard' margin="dense" fullWidth type='email' id="email_address" autoComplete='off' placeholder='Email Address' {...register('email_address', { pattern: { value: EMAIL_REGEX, message: 'Invalid email address' }, validate: value => isSupportedDomain(value) || 'Email domain not supported', required: { value: true, message: 'Email address is required' }, })} /> {errors.email_address &&( <p id="errMsg"> <MUI.InfoIcon className="infoErr" /> {errors.email_address.message} </p> )} </MUI.Grid> <br/> <br/> {/* Password Textfield */} <MUI.Grid item> <MUI.TextField variant='standard' margin="dense" fullWidth type={showPassword ? 'text' : 'password'} id="password" placeholder='Password' InputProps={{ endAdornment: ( <MUI.InputAdornment position="end"> <MUI.IconButton onClick={handleTogglePassword} edge="end"> {showPassword ? <MUI.VisibilityIcon sx={{ fontSize: '1.2rem' }} /> : <MUI.VisibilityOffIcon sx={{ fontSize: '1.2rem' }} /> } </MUI.IconButton> </MUI.InputAdornment> ), }} {...register('password', { required: { value: true, message: 'Password is required' }, })} /> {errors.password &&( <p id="errMsg"> <MUI.InfoIcon className="infoErr" /> {errors.password.message} </p> )} </MUI.Grid> <br/> {/* Remember Me Checkbox */} <MUI.FormControlLabel control={<MUI.Checkbox value="remember" color="primary" />} label="Remember me" /> {/* Login Button */} <MUI.Button type="submit" fullWidth variant="contained" sx={{ mt: 3, mb: 2, color: 'white', backgroundColor: '#311b92', borderRadius: '10px' }} > Sign In </MUI.Button> {/* Forgot Password Link */} <MUI.Grid container> <MUI.Grid item xs> <MUI.Link href="#" variant="body2" style={{ textDecoration: 'none' }}> Forgot password? </MUI.Link> </MUI.Grid> </MUI.Grid> </MUI.Box> </MUI.Box> </MUI.Grid> <DevTool control={control} /> </MUI.Grid> </MUI.ThemeProvider> ) }
API.php
//Protected Route Route::group(['middleware' => ['auth:sanctum']], function() { Route::post('/scholars', [ScholarController::class, 'store']); Route::put('/scholars/{user_id}', [ScholarController::class, 'update']); Route::delete('/scholars/{user_id}', [ScholarController::class, 'destroy']); Route::post('/users', [UserController::class, 'store']); Route::put('/users/{id}', [UserController::class, 'update']); Route::delete('/users/{firstname}', [UserController::class, 'destroy']); Route::post('/logout', [AuthController::class, 'logout']); Route::post('/register', [AuthController::class, 'register']); Route::apiResource('/scholars', ScholarController::class)->only(['index', 'show']); Route::apiResource('/users', UserController::class)->only(['index', 'show']); Route::apiResource('/roles', RoleController::class)->only(['index', 'show']); Route::get('/scholars/search/{user_id}', [ScholarController::class, 'search']); Route::get('/users/search/{firstname}', [UserController::class, 'search']); // Route::get('/refresh-token', [AuthController::class,'refreshToken']); }); //Public Route Route::post('/login', [AuthController::class, 'login']); Route::middleware('auth:api')->get('/scholar', function (Request $request){ return $request->scholar();; }); Route::middleware('auth:api')->get('/user', function (Request $request){ return $request->scholar();; });
问题修复方案
1. 前端状态持久化缺失(核心问题)
页面刷新时useAuth的状态会重置,导致auth?.role_id为undefined,触发登录页重定向。需要修改useAuth钩子,从localStorage恢复状态:
// hooks/useAuth.jsx import { useState, useEffect } from 'react'; const useAuth = () => { // 初始化时从localStorage加载保存的登录状态 const [auth, setAuth] = useState(() => { const savedAuth = localStorage.getItem('auth'); return savedAuth ? JSON.parse(savedAuth) : {}; }); // 状态变化时同步到localStorage useEffect(() => { localStorage.setItem('auth', JSON.stringify(auth)); }, [auth]); return { auth, setAuth }; }; export default useAuth;
同时修改Login.jsx的状态保存逻辑,把完整的auth对象存入localStorage:
// Login.jsx 的onSubmit函数内 const authData = { user, remember_token, roles_name, role_id, expirationTime }; setAuth(authData); // 替换原有的单个token存储 localStorage.setItem('auth', JSON.stringify(authData));
2. Axios请求未自动携带token
配置Axios实例,自动在请求头中添加Authorization:
// api/axios.js import axios from 'axios'; const instance = axios.create({ baseURL: '/api', withCredentials: true, // 适配Sanctum的cookie验证 }); // 请求拦截器,自动添加token instance.interceptors.request.use((config) => { const auth = JSON.parse(localStorage.getItem('auth')); if (auth?.remember_token) { config.headers.Authorization = `Bearer ${auth.remember_token}`; } return config; }); export default instance;
3. Laravel路由中间件错误
API.php中混用了auth:api(Passport中间件)和auth:sanctum,统一改为auth:sanctum,并修正错误的$request->scholar():
// 替换原有的两个测试路由 Route::middleware('auth:sanctum')->get('/user', function (Request $request){ return $request->user(); });
4. Sanctum配置检查
确保.env中正确配置状态域:
SANCTUM_STATEFUL_DOMAINS=localhost:3000 # 你的前端端口 SESSION_DOMAIN=.localhost
内容的提问来源于stack exchange,提问作者Christian Medallada
相关产品推荐
相关产品推荐

