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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 20:15:53