React+Express登录请求中JWT Token为undefined问题求助
问题描述
我使用React前端+Express后端开发Web应用,通过JSON Web Token(JWT)实现用户认证。登录请求发送后服务器返回成功,但Token始终为undefined,已排查服务器响应并通过console.log调试,仍无法定位问题根源。
前端代码
import { useContext, useState } from "react"; import { Navigate } from "react-router-dom"; import { UserContext } from "../UserContext"; export default function LoginPage() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [redirect, setRedirect] = useState(false); const { setUserInfo } = useContext(UserContext); async function login(ev) { ev.preventDefault(); const response = await fetch('https://personal-website-on6a.onrender.com/login', { method: 'POST', body: JSON.stringify({ username, password }), headers: { 'Content-Type': 'application/json' }, credentials: 'include', withCredentials: true, }); if (response.ok) { response.json().then(userInfo => { setUserInfo(userInfo); setRedirect(true); }); } else { alert('wrong credentials'); } } if (redirect) { return <Navigate to={'/'} /> } return ( <form className="login" onSubmit={login}> <h1>Login</h1> <div className="form-container"> <input type="text" placeholder="username" value={username} onChange={ev => setUsername(ev.target.value)} /> <input type="password" placeholder="password" value={password} onChange={ev => setPassword(ev.target.value)} /> <div style={{ width: '400px' }}> <button className="btn ac_btn">Login</button> </div> </div> </form> ); }
后端代码
const express = require('express'); const cors = require('cors'); const mongoose = require("mongoose"); const User = require('./models/User'); const Post = require('./models/Post'); const bcrypt = require('bcryptjs'); const app = express(); const jwt = require('jsonwebtoken'); const cookieParser = require('cookie-parser'); const multer = require('multer'); const uploadMiddleware = multer({ storage: multer.memoryStorage() }); const admin = require('firebase-admin'); require('dotenv').config(); const serviceAccount = { projectId: process.env.FIREBASE_PROJECT_ID, clientEmail: process.env.FIREBASE_CLIENT_EMAIL, privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'), }; admin.initializeApp({ credential: admin.credential.cert(serviceAccount), storageBucket: process.env.FIREBASE_BUCKET, }); const bucket = admin.storage().bucket(); const salt = bcrypt.genSaltSync(10); const secret = process.env.JWT_SECRET; const domain = 'https://personal-website-ea41b.web.app'; app.use(cors({ origin: domain, credentials: true, optionsSuccessStatus: 200 })); app.use(cookieParser()); app.use(express.json()); const uri = process.env.MONGO_URL; mongoose.connect(uri, { serverSelectionTimeoutMS: 30000, }); app.post('/register', async (req, res) => { const { username, password } = req.body; try { const userDoc = await User.create({ username, password: bcrypt.hashSync(password, salt), }); res.json(userDoc); } catch (e) { console.log(e); res.status(400).json(e); } }); app.post('/login', async (req, res) => { const { username, password } = req.body; const userDoc = await User.findOne({ username }); const passOk = bcrypt.compareSync(password, userDoc.password); if (passOk) { jwt.sign({ username, id: userDoc._id }, secret, {}, (err, token) => { console.log('token-secret', secret, token) if (err) throw err; res.cookie('token', token).json({ id: userDoc._id, username, }); }); } else { res.status(400).json('wrong credentials'); } }); app.get('/profile', (req, res) => { const { token } = req.cookies; console.log('profile token', token); jwt.verify(token, secret, {}, (err, info) => { if (err) throw err; res.json(info); }); }); app.post('/logout', (req, res) => { res.cookie('token', '').json('ok'); }); app.post('/post', uploadMiddleware.single('file'), async (req, res) => { const { originalname } = req.file; const { title, summary, content } = req.body; const fileUpload = bucket .file(`blog_covers/` + originalname); const blobStream = fileUpload.createWriteStream({ metadata: { contentType: req.file.mimetype, }, }); blobStream.on('error', (err) => { console.error(err); res.status(500).json('Error uploading file'); }); blobStream.on('finish', async () => { const [url] = await fileUpload.getSignedUrl({ action: 'read', expires: '03-01-2500', }); const { token } = req.cookies; jwt.verify(token, secret, {}, async (err, info) => { if (err) throw err; const postDoc = await Post.create({ title, summary, content, cover: url, author: info.id, }); res.json(postDoc); }); }); blobStream.end(req.file.buffer); }); app.put('/post', uploadMiddleware.single('file'), async (req, res) => { const { id, title, summary, content } = req.body; const postDoc = await Post.findById(id); const isAuthor = JSON.stringify(postDoc.author) === JSON.stringify(info.id); if (!isAuthor) { return res.status(400).json('you are not the author'); } const fileUpload = bucket.file(req.file.originalname); const blobStream = fileUpload.createWriteStream({ metadata: { contentType: req.file.mimetype, }, }); blobStream.on('error', (err) => { console.error(err); res.status(500).json('Error uploading file'); }); blobStream.on('finish', async () => { const [url] = await fileUpload.getSignedUrl({ action: 'read', expires: '03-01-2500', }); const { token } = req.cookies; jwt.verify(token, secret, {}, async (err, decodedToken) => { if (err) throw err; await postDoc.update({ title, summary, content, cover: url ? url : postDoc.cover, }); res.json(postDoc); }); }); blobStream.end(req.file.buffer); }); app.get('/post', async (req, res) => { res.json( await Post.find() .populate('author', ['username']) .sort({ createdAt: -1 }) .limit(20) ); }); app.get('/post/:id', async (req, res) => { const { id } = req.params; const postDoc = await Post.findById(id).populate('author', ['username']); res.json(postDoc); }); app.listen(4000, () => { console.log('Server started on port 4000'); });
问题分析与解决方案
1. Cookie缺失跨域安全属性(核心原因)
你的前后端属于跨域场景(前端web.app,后端render.com),后端设置Cookie时未指定secure、sameSite等强制安全属性,浏览器会拒绝保存Cookie,导致后续请求无法携带Token。
修复方案:修改后端/login接口的Cookie配置,添加必要属性:
// 替换原res.cookie代码 res.cookie('token', token, { httpOnly: true, // 防止XSS攻击,前端JS无法直接读取但不影响自动携带 secure: true, // 仅在HTTPS环境下传输,你的部署环境符合要求 sameSite: 'none', // 跨域场景必须设为none,需配合secure属性使用 maxAge: 24 * 60 * 60 * 1000 // 可选,设置Cookie有效期1天 }).json({ id: userDoc._id, username, });
2. 前端Fetch冗余配置冲突
Fetch API中只需credentials: 'include'即可开启跨域携带Cookie,你额外添加的withCredentials: true是XMLHttpRequest的属性,属于冗余配置,可能导致逻辑冲突。
修复方案:删除前端Fetch中的withCredentials: true:
const response = await fetch('https://personal-website-on6a.onrender.com/login', { method: 'POST', body: JSON.stringify({ username, password }), headers: { 'Content-Type': 'application/json' }, credentials: 'include', // 仅保留这一行 });
3. 后端PUT接口逻辑错误(额外问题)
你的PUT /post接口中,验证作者身份时提前使用了未定义的info变量,会导致info is not defined报错,影响文章编辑功能。
修复方案:调整逻辑顺序,先验证Token拿到info再做身份判断:
app.put('/post', uploadMiddleware.single('file'), async (req, res) => { const { id, title, summary, content } = req.body; const { token } = req.cookies; // 先验证Token获取用户信息 jwt.verify(token, secret, {}, async (err, info) => { if (err) throw err; const postDoc = await Post.findById(id); const isAuthor = JSON.stringify(postDoc.author) === JSON.stringify(info.id); if (!isAuthor) { return res.status(400).json('you are not the author'); } // 剩余文件上传及更新逻辑... }); });
内容的提问来源于stack exchange,提问作者Sarthak
相关产品推荐
相关产品推荐

