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

MERN栈中Passport.js实现Google OAuth2.0自动登录问题求助

Google OAuth2.0 自动登录问题(MERN + Passport.js + JWT)

我是一名开发经验不足一年的开发者,基于MERN栈和JSON Web Token(JWT),使用Passport.js实现Google OAuth2.0认证。目前注册功能正常,用户可成功存入MongoDB,但自动登录环节失效——无法将Google用户信息传递到前端完成自动登录。尝试过配置CORS、在Cookie/URL中传递JWT、后端请求Google等方法,均未解决问题。附上前后端代码寻求解决方案:


后端代码

import express from "express";
import "dotenv/config";
import mongoose from "mongoose";
import cors from "cors";
import multer from "multer";
import productRouter from "./routes/productsRoute";
import userRouter from "./routes/userRoute";
import paymentRouter from "./routes/paymentRoute";
import adminRouter from "./routes/adminRoute";
import { auth, generateToken } from "./middlewares/auth";
import passport from "passport";
import session from "express-session";
import User from "./models/userModel";
import axios from "axios";
import cookieParser from "cookie-parser";

main().catch((err) => console.log(err));

async function main() {
  await mongoose.connect("mongodb://127.0.0.1:27017/EcomDB");
  console.log(`[DATABASE] MongoDB is connected ⚡️`);
}

const app = express();
const port = process.env.PORT;
const { Strategy: GoogleStrategy } = require("passport-google-oauth20");
const upload = multer({ dest: "uploads/" });

const corsOptions = {
  origin: ["http://localhost:5173"],
  methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
  allowedHeaders: ["Content-Type", "Authorization"],
  credentials: true,
};

app.use(cors(corsOptions));

app.use(cookieParser());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static("./uploads/images"));
app.use(
  session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: true,
    cookie: { maxAge: 1000 * 60 * 60 * 24 },
  })
);
app.use(passport.initialize());
app.use(passport.session());

passport.use(
  new GoogleStrategy(
    {
      clientID: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      callbackURL: "http://localhost:3456/auth/google/callback",
      scope: ["email", "profile"],
    },
    async function (accessToken, refreshToken, profile, cb) {
      try {
        const googleId = profile.id;
        const email = profile.emails[0].value;
        const firstname = profile.name.givenName;
        const lastname = profile.name.familyName;
        const password = "azerty";
        const zipCode = 10000;
        const Adress = "Adresse par défaut";
        const role = "user";
        const userCart = [];
        let googleUser = await User.findOne({ email });

        if (!googleUser) {
          googleUser = await User.create({
            googleId,
            email,
            firstname,
            lastname,
            password,
            zipCode,
            Adress,
            role,
            userCart,
          });
          googleUser.password = await googleUser.encryptedPassword(password);
          googleUser.save();
          const token = generateToken(googleUser.id);
          console.log("user successfully sign");
          return cb(null, { googleUser, token });
        } else {
          const token = generateToken(googleUser.id);
          console.log("user successfully logged in");
          googleUser.token = token;
          return cb(null, googleUser, token);
        }
      } catch (error) {
        return cb(error);
      }
    }
  )
);

passport.serializeUser(function (user, done) {
  done(null, user);
});
passport.deserializeUser(async function (user, done) {
  const googleuser = await User.findById(user.id);
  done(null, googleuser);
});

app.get("/", (req, res) => {
  res.json("Welcome");
});

app.get(
  "/auth/google/login",
  passport.authenticate("google", { scope: ["email", "profile"] })
);

app.get(
  "/auth/google/callback",
  passport.authenticate("google", {
    failureRedirect: "http://localhost:5173/sign-in",
  }),
  (req, res) => {
    const user = req.user.googleUser || req.user;
    const token = req.user.token || generateToken(user.id);

    res.cookie("jwtToken", token, { httpOnly: true, secure: false });
    res.redirect("http://localhost:5173");
  }
);

app.use("/users", userRouter);
app.use("/products", productRouter);
app.use("/payment", paymentRouter);
app.use("/admin", auth, adminRouter);

app.listen(port, () =>
  console.log(`[SERVER] Listening on http://localhost:${port}`)
);

前端代码

import React, { useState, useEffect } from "react";
import axios from "axios";
import { Link, useNavigate } from "react-router-dom";
import { jwtDecode } from "jwt-decode";
import { useSnackbar } from "notistack";
import { userInfos } from "../context";

function Login() {
  const { enqueueSnackbar } = useSnackbar();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const { setUsername, setToken, setUserId, setUserRole } = userInfos();
  const navigate = useNavigate();

  useEffect(() => {
    const storedToken = localStorage.getItem("token");
    setToken(storedToken);
  }, []);

  const handleGoogleLogin = () => {
    console.log(
      (window.location.href = `http://localhost:3456/auth/google/login`)
    );

    window.location.href = `http://localhost:3456/auth©/google/login`;
  };


  const handleUserLogin = async (e) => {
    e.preventDefault();
    try {
      const response = await axios.post(
        `${import.meta.env.VITE_SERVER_URL}/users/connexion`,
        { email, password }
      );
      console.log(response.data);
      const { token } = response.data;
      const decodedUser = jwtDecode(token);
      localStorage.setItem("user", JSON.stringify(decodedUser));
      localStorage.setItem("token", JSON.stringify(response.data.token));
      setToken(response.data.token);
      setUserId(decodedUser.id);
      setUserRole(decodedUser.role);
      setUsername(decodedUser.name);
      enqueueSnackbar("Connexion successfully completed", {
        variant: "success",
        autoHideDuration: 2000,
      });
      navigate("/");
    } catch (error) {
      enqueueSnackbar("Connexion failed please verify your email or password", {
        variant: "error",
        autoHideDuration: 3000,
      });
      console.error(error);
    }
  };

  // 原代码此处可能缺失返回JSX部分
}

export default Login;

问题分析与修复方案

1. 后端Passport回调与序列化逻辑错误

  • 问题:Passport的verify回调中,新用户返回嵌套对象{ googleUser, token },但serializeUser直接存储整个对象,导致deserializeUser中无法正确获取user.id;已有用户分支返回参数数量错误(cb(null, googleUser, token)不符合Passport回调规范)。
  • 修复:
    // 修改Passport verify回调逻辑
    async function (accessToken, refreshToken, profile, cb) {
      try {
        // ... 省略用户查询/创建逻辑
        if (!googleUser) {
          // ... 省略用户创建与密码加密逻辑
          const token = generateToken(googleUser.id);
          googleUser.token = token; // 将token直接挂载到用户对象上
          return cb(null, googleUser); // 仅返回用户对象
        } else {
          const token = generateToken(googleUser.id);
          googleUser.token = token;
          return cb(null, googleUser); // 统一返回格式
        }
      } catch (error) {
        return cb(error);
      }
    }
    
    // 修正serializeUser,仅存储用户ID(减少session体积)
    passport.serializeUser(function(user, done) {
      done(null, user.id);
    });
    

2. 前端Google登录URL拼写错误

  • 问题:handleGoogleLogin中存在错误URL(包含特殊字符©),导致跳转失败。
  • 修复:
    const handleGoogleLogin = () => {
      window.location.href = `http://localhost:3456/auth/google/login`;
    };
    

3. 回调路由的JWT传递与前端状态同步缺失

  • 问题:后端设置了httpOnly Cookie,但前端跳转后未主动验证Cookie或读取token同步登录状态。
  • 修复:
    • 后端回调路由优化Cookie配置,并在跳转URL附加token(作为备选方案):
      app.get(
        "/auth/google/callback",
        passport.authenticate("google", { failureRedirect: "http://localhost:5173/sign-in" }),
        (req, res) => {
          const token = req.user.token || generateToken(req.user.id);
          // 添加sameSite适配跨域场景
          res.cookie("jwtToken", token, { httpOnly: true, secure: false, sameSite: "Lax" });
          // 跳转时在URL携带token,方便前端快速获取
          res.redirect(`http://localhost:5173?token=${token}`);
        }
      );
      
    • 前端在首页添加状态同步逻辑:
      // 在App.js或首页组件的useEffect中添加
      useEffect(() => {
        // 读取URL中的token
        const urlParams = new URLSearchParams(window.location.search);
        const urlToken = urlParams.get('token');
        if (urlToken) {
          const decoded = jwtDecode(urlToken);
          localStorage.setItem('token', urlToken);
          setToken(urlToken);
          setUserId(decoded.id);
          setUserRole(decoded.role);
          setUsername(`${decoded.firstname} ${decoded.lastname}`);
          // 清除URL参数,避免重复处理
          urlParams.delete('token');
          window.history.replaceState({}, document.title, window.location.pathname + (urlParams.toString() ? `?${urlParams.toString()}` : ''));
        }
      
        // 验证httpOnly Cookie中的token(需后端配合提供验证接口)
        const checkAuthCookie = async () => {
          try {
            const res = await axios.get(`${import.meta.env.VITE_SERVER_URL}/users/verify-auth`, { withCredentials: true });
            if (res.data.success) {
              const { token, user } = res.data;
              localStorage.setItem('token', token);
              setToken(token);
              setUserId(user.id);
              setUserRole(user.role);
              setUsername(`${user.firstname} ${user.lastname}`);
            }
          } catch (err) {
            console.log('无有效认证Cookie');
          }
        };
        checkAuthCookie();
      }, [setToken, setUserId, setUserRole, setUsername]);
      

4. 移除冗余的Session配置(可选)

既然使用JWT作为认证凭证,可禁用Passport的Session功能,避免冗余存储:

// 移除session中间件
// app.use(session({...}));
app.use(passport.initialize());
// 移除app.use(passport.session());

// 修改Passport认证路由的session选项为false
app.get(
  "/auth/google/login",
  passport.authenticate("google", { scope: ["email", "profile"], session: false })
);
app.get(
  "/auth/google/callback",
  passport.authenticate("google", { failureRedirect: "http://localhost:5173/sign-in", session: false }),
  (req, res) => {
    // 原有回调逻辑...
  }
);

内容的提问来源于stack exchange,提问作者Raphael Garnier

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 17:49:52