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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 03:44:52