登录表单与受保护路由异常:登录成功后无法停留/tickets路由
登录成功后跳转异常问题:短暂进入/tickets后返回/admin,localStorage未存入token
我实现了登录表单与受保护路由功能,提交表单时用户名和密码能正常发送到服务器,且服务器验证通过。但服务器返回token后,页面短暂跳转至/tickets路由后又回到/admin路由,无法停留在目标路由。检查发现localStorage并未成功存入token,推测这是问题根源。当前仅为学习目的,暂不考虑安全性问题。
相关代码
protected.jsx
import React from "react"; import { Outlet, Navigate } from "react-router-dom"; //Create a protected route, that can only be accessed with admin credentials //For the prototype this will do function Protected() { const token = localStorage.getItem('token'); console.log(localStorage.getItem('token')); //the /admin route is the route to the login form. return token ? <Outlet /> : <Navigate to='/admin' />; } export default Protected;
service.jsx
import React from "react"; import Home from "../Home"; import TicketingSystem from "./TicketingSystem"; import Login from "./Login"; import Protected from "./Protected"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; //Creates protected routes, that will only allow authorized users to access with authorized credentials. //At this point it will be hard coded, securely into the server function Service(props) { return ( <Router> <Routes> <Route path='/admin' element={<Login />} /> <Route path='/' element={<Home />} /> <Route element={<Protected />}> <Route path='/tickets' element={<TicketingSystem />} /> </Route> </Routes> </Router> ); } export default Service;
login.jsx
import React, { useState } from "react"; import { Grid, Typography, TextField, Button } from "@mui/material"; import "./media/login.css"; function Login(props) { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const handleSubmit = async (event) => { event.preventDefault(); const res = await fetch ('/login-request', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); if (res.ok) { const { token } = await res.json(); localStorage.setItem('token', token); props.history.push('/tickets') } } return ( <React.Fragment> <Grid container justifyContent='center' alignItems='center'> <div id='login-block'> <Typography align='center'> <h1 id='login-title'>Admin Login</h1> <form onSubmit={handleSubmit}> <Grid item xs={12}> <TextField id='username' name='username' variant='standard' label='username' size='small' /> <br /> </Grid> <Grid item xs={12}> <br /> <TextField id='password' type='password' name='password' variant='standard' label='password' size='small' /> <br /> </Grid> <Grid item xs={12}> <br /> <Button id='submit-login-request' variant='contained' type='submit'> Login </Button> </Grid> </form> <p id='l1'>The unauthorized use of this route, is illegal.</p> </Typography> </div> </Grid> </React.Fragment> ); } export default Login;
server.js
app.post('/login-request', (req, res, next) => { /* You need to update the token in the front end to true to access tickets, But this is insecure, make sure the password is hashed and sent securely make sure users cant find vulnerabilities in the login form. ' */ console.log('recieved login request', req.body.username, req.body.password); const { username, password } = req.body; if (username === "admin" && password === "password") { const token = jwt.sign({ username }, 'test'); res.cookie('jwt', token, { httpOnly: true }); // Redirect the user to the /tickets route res.status(302).setHeader('Location', '/tickets').end(); } else { res.status(401).json({message: "invalid credentials"}); } });
问题原因及修复方案
1. 服务器端错误:返回302重定向而非JSON数据
当前服务器验证成功后返回302重定向头,前端fetch会自动跟随重定向请求/tickets的HTML页面,而非预期的JSON格式token。这导致res.json()执行失败,后续存入token和路由跳转的代码都不会运行,最终受保护路由检测不到token,强制跳回/admin。
修复服务器代码:
删除重定向逻辑,返回包含token的JSON响应:
app.post('/login-request', (req, res, next) => { console.log('recieved login request', req.body.username, req.body.password); const { username, password } = req.body; if (username === "admin" && password === "password") { const token = jwt.sign({ username }, 'test'); res.cookie('jwt', token, { httpOnly: true }); // 返回JSON格式token,由前端处理跳转 res.status(200).json({ token }); } else { res.status(401).json({message: "invalid credentials"}); } });
2. 前端Login组件:路由跳转方式错误+状态未绑定
React Router v6中不再通过props.history获取路由对象,需使用useNavigate钩子;同时TextField未绑定状态更新,可能导致发送空的用户名/密码(即使当前服务器验证通过,也是潜在问题)。
修复Login.jsx代码:
import React, { useState } from "react"; import { Grid, Typography, TextField, Button } from "@mui/material"; import { useNavigate } from "react-router-dom"; import "./media/login.css"; function Login() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const navigate = useNavigate(); const handleSubmit = async (event) => { event.preventDefault(); try { const res = await fetch ('/login-request', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); if (res.ok) { const { token } = await res.json(); localStorage.setItem('token', token); navigate('/tickets'); } else { const error = await res.json(); console.error(error.message); } } catch (err) { console.error('登录请求失败:', err); } } return ( <React.Fragment> <Grid container justifyContent='center' alignItems='center'> <div id='login-block'> <Typography align='center'> <h1 id='login-title'>Admin Login</h1> <form onSubmit={handleSubmit}> <Grid item xs={12}> <TextField id='username' name='username' variant='standard' label='username' size='small' value={username} onChange={(e) => setUsername(e.target.value)} /> <br /> </Grid> <Grid item xs={12}> <br /> <TextField id='password' type='password' name='password' variant='standard' label='password' size='small' value={password} onChange={(e) => setPassword(e.target.value)} /> <br /> </Grid> <Grid item xs={12}> <br /> <Button id='submit-login-request' variant='contained' type='submit'> Login </Button> </Grid> </form> <p id='l1'>The unauthorized use of this route, is illegal.</p> </Typography> </div> </Grid> </React.Fragment> ); } export default Login;
3. 可选优化:Protected组件监听token变化
若后续遇到token更新后组件未重新渲染的问题,可添加useEffect监听localStorage变化:
import React, { useState, useEffect } from "react"; import { Outlet, Navigate } from "react-router-dom"; function Protected() { const [token, setToken] = useState(localStorage.getItem('token')); useEffect(() => { const handleStorageChange = () => { setToken(localStorage.getItem('token')); }; window.addEventListener('storage', handleStorageChange); return () => window.removeEventListener('storage', handleStorageChange); }, []); console.log(token); return token ? <Outlet /> : <Navigate to='/admin' />; } export default Protected;
内容的提问来源于stack exchange,提问作者Pylot
相关产品推荐
相关产品推荐

