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传递与前端状态同步缺失
- 问题:后端设置了
httpOnlyCookie,但前端跳转后未主动验证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]);
- 后端回调路由优化Cookie配置,并在跳转URL附加token(作为备选方案):
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
相关产品推荐
相关产品推荐

