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

MERN栈部署Vercel后JWT Cookie刷新丢失问题求助

MERN栈部署Vercel后刷新丢失登录状态问题解决

问题概述

将MERN栈网站部署至Vercel(前端与API均部署)后,登录成功页面右上角显示“Welcome User”,但刷新页面会回到登录/注册状态。本地localhost:3000环境完全正常,刷新时JWT token可正常存储在Cookie中,仅部署到Vercel后出现该问题。

相关代码配置

后端导入模块配置

const express = require('express');
const app = express();
const path = require('path')
const mongoose = require("mongoose")
const cors = require('cors')
const bcrypt = require('bcrypt')
const jwt = require('jsonwebtoken')
const UserModel = require('./models/User')
const CookieParser = require('cookie-parser')
const multer = require('multer') 
const dotenv = require('dotenv')
const PostModel = require("./models/Post.js")

// app.use(cors({credentials : true , origin : 'https://blogifyv1.vercel.app'}))
app.use(cors({credentials : true , origin : true}))
app.use(express.json())
app.use(CookieParser())
app.use('/uploads',express.static(__dirname + '/uploads')) // 用于从uploads文件夹展示图片

app.use(express.urlencoded({
    extended : false
}))

登录路由

添加domain前的代码

app.post('/login',async(req,res)=>{
    const {username , password} = req.body;
    try {
        const userDoc = await UserModel.findOne({username : username})
        const passwordMatched = await bcrypt.compareSync(password , userDoc.password)
        console.log("isValid",passwordMatched)

        // 生成token
        if(passwordMatched){
            jwt.sign({username , id:userDoc._id}, secretSalt,{},(err,token)=>{
                if(err){
                    throw err;
                }
                else{
                    res.cookie('token',token).json({
                        id : userDoc._id,
                        username
                    })
                }
            })
        }
        else{
            res.status(400).json("Wrong Credentials")
        }
    } catch (err) {
        console.log(err)
        res.status(400).json(err)
    }
})

添加domain后的代码

app.post('/login',async(req,res)=>{
    const {username , password} = req.body;
    try {
        const userDoc = await UserModel.findOne({username : username})
        const passwordMatched = await bcrypt.compareSync(password , userDoc.password)
        console.log("isValid",passwordMatched)

        // 生成token
        if(passwordMatched){
            jwt.sign({username , id:userDoc._id}, secretSalt,{},(err,token)=>{
                if(err){
                    throw err;
                }
                else{
                    res.cookie('token',token,{ domain: '.vercel.app', secure: true }).json({
                        id : userDoc._id,
                        username
                    })
                }
            })
        }
        else{
            res.status(400).json("Wrong Credentials")
        }
    } catch (err) {
        console.log(err)
        res.status(400).json(err)
    }
})

创建文章路由及登出接口

// 登出接口
app.post('/logout',async(req,res)=>{
    res.cookie('token' , "").json("ok")
})

// 创建文章接口
app.post('/post',uploadMiddleware.single('file'),async(req,res)=>{// 'file'对应CreatePost.js中data.set('file',file)的键
    res.json({files : req.file})
    console.log("file", req.file)
    console.log("filename", req.file?.filename)
    const {token} = req.cookies

    jwt.verify(token,secretSalt,{},async (err,info)=>{
        if(err) throw err;

        const  {title , summary,content} = req.body;
        const postDoc = await PostModel.create({
            title,
            summary,
            content,
            cover : req.file.filename,
            author : info.id,
        })
        res.json(postDoc)
    })
})

React登录组件(Login.js)

import React, { useContext, useState } from 'react'
import Navbar from '../components/Navbar'
import Form from '../components/Form'
import { useNavigate } from 'react-router';
import { UserContext } from '../contextapi/UserContext';

const Login = () => {
  const [username , setUsername] = useState('');
  const [password , setPassword] = useState('');
  const navigate = useNavigate();

  const {setUserInfo} = useContext(UserContext);

  const login=async(e)=>{
    e.preventDefault();
    try {
        const response = await fetch('https://mern-blog-api-rho.vercel.app/login',{
            method: "POST",
            body : JSON.stringify({username , password}),
            headers: {'Content-type' : 'application/json'},
            credentials: "include",  // 允许携带Cookie
        })
        console.log(response)
        if(response.status === 200){
          response.json().then((userInfo)=>{
            setUserInfo(userInfo)
            navigate('/')
          })
        }
        else{
            alert('Registration Failed , try again later')
        }
    } catch (error) {
        console.log(error)
    }
}

Vercel.json配置

{
    "version":2,
    "builds":[
        {"src":"./index.js","use":"@vercel/node"}
    ],
    "routes": [
        {
          "src": "/(.*)",
          "dest": "/",
          "methods": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
          "headers": {
            "Access-Control-Allow-Origin": "*"
          }
        }
    ]
}

修复方案

1. 修正CORS配置

将后端CORS配置改为明确指定前端域名,避免通配符与credentials: true冲突:

app.use(cors({
  credentials: true,
  origin: 'https://你的前端Vercel域名' // 例如https://blogifyv1.vercel.app
}))

同时删除vercel.json中Access-Control-Allow-Origin: "*"的配置,由CORS中间件统一处理跨域头。

2. 调整Cookie属性设置

删除domain: '.vercel.app',补充sameSite和httpOnly属性,适配跨域场景:

// 登录接口的Cookie设置
res.cookie('token', token, {
  secure: true, // HTTPS环境必须开启
  sameSite: 'none', // 跨域场景必须设置为none
  httpOnly: true, // 可选,提升安全性,防止XSS
  maxAge: 24 * 60 * 60 * 1000 // 可选,设置Cookie有效期,比如1天
}).json({
  id: userDoc._id,
  username
})

// 登出接口的Cookie设置
app.post('/logout',async(req,res)=>{
  res.cookie('token', "", {
    secure: true,
    sameSite: 'none',
    httpOnly: true,
    expires: new Date(0) // 立即过期
  }).json("ok")
})

3. 前端添加页面刷新时的身份校验

在前端根组件(如App.js)中添加初始化校验,刷新页面时自动验证Cookie中的token:

后端新增校验接口

app.get('/profile', (req, res) => {
  const { token } = req.cookies;
  if (!token) return res.status(401).json("未登录");
  jwt.verify(token, secretSalt, {}, (err, info) => {
    if (err) return res.status(401).json("token无效");
    res.json(info);
  });
})

前端根组件添加校验逻辑

import { useContext, useEffect } from 'react';
import { UserContext } from './contextapi/UserContext';

function App() {
  const { setUserInfo } = useContext(UserContext);
  
  useEffect(() => {
    // 页面加载时校验登录状态
    fetch('https://mern-blog-api-rho.vercel.app/profile', {
      credentials: "include"
    }).then(res => {
      if (res.status === 200) {
        res.json().then(userInfo => setUserInfo(userInfo));
      }
    }).catch(err => console.error(err));
  }, []);
  
  // ...其他组件渲染逻辑
}

export default App;

4. 更新Vercel配置

修改vercel.json,移除冲突的跨域头配置:

{
  "version":2,
  "builds":[
    {"src":"./index.js","use":"@vercel/node"}
  ],
  "routes": [
    {
      "src": "/(.*)",
      "dest": "/",
      "methods": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]
    }
  ]
}

内容的提问来源于stack exchange,提问作者Yash Kamble

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 05:44:52