我已经研究这个错误超过 20 个小时了,我可以详细分解它,但无法解决它,所以任何帮助将不胜感激。
目前,在 Google OAuth 期间成功重定向后,Req.user 和 req.session 不会持续存在,但它们确实会持续使用其他护照进行本地电子邮件/密码登录。 Google OAuth 在使用 localhost 和 docker 进行开发期间也可以正常工作。它是一个 Mongo/Express/React/Node 应用程序,前端托管在 Netlify 上,后端托管在 Heroku 上。数据库位于 MongoDB Atlas 上。
生产仓库:https://github.com/normnXT/note-keeper/tree/product
如果可能的话,请查看生产存储库,但这里是一部分一部分。客户端的登录功能:
const onGoogleLogin = () => {
try {
window.open(`${process.env.REACT_APP_SERVER_URL}/api/auth/google`, "_self");
} catch (err) {
toast.error(err.response.data);
}
};
谷歌oauth路线:
router.get("/google", passport.authenticate("google", ["profile", "email"]));
router.get(
"/google/callback",
passport.authenticate("google", { failureRedirect: '/login/failed', prompt: 'consent', accessType: 'offline' }),
(req, res) => {
console.log("Session after Google auth:", req.session);
console.log("User after Google auth:", req.user);
res.redirect(process.env.CLIENT_URL);
},
);
server.js 文件:
const express = require("express");
const session = require("express-session");
const localRouter = require("./routes/local");
const noteRouter = require("./routes/notes");
const authRouter = require("./routes/auth");
const User = require("./models/User");
const MongoStore = require("connect-mongo");
const mongoose = require("mongoose");
const cors = require("cors");
const passport = require("passport");
const bcrypt = require("bcryptjs");
const LocalStrategy = require("passport-local").Strategy;
const OAuthStrategy = require("passport-google-oauth20").Strategy;
const app = express();
app.use(express.json());
// Enables cross-origin resource sharing between API's and client
app.use(
cors({
origin: process.env.CLIENT_URL,
methods: "GET,POST,PUT,DELETE",
credentials: true,
}),
);
// Sets up express session to store user data server side and on MongoDB Atlas
app.set("trust proxy", 1);
app.use(
session({
secret: process.env.SESSION_SECRET,
resave: false, // Session will only be resaved if it is modified
saveUninitialized: false, // Sessions will only be saved once initialized
proxy: true,
secure: true,
cookie: {
secure: true,
maxAge: 1000 * 60 * 60 * 24,
sameSite: "none",
httpOnly: true,
},
store: MongoStore.create({ mongoUrl: process.env.MONGO_URI }),
}),
);
// Initializes passport session
app.use(passport.initialize());
app.use(passport.session());
// Google passport strategy for OAuth 2.0 login
// https://developers.google.com/identity/protocols/oauth2
passport.use(
new OAuthStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: `${process.env.SERVER_URL}/api/auth/google/callback`,
proxy: true,
scope: ["profile", "email"],
},
async (accessToken, refreshToken, profile, done) => {
console.log({ accessToken, refreshToken, ...profile});
try {
let user = await User.findOne({
email: profile.emails[0].value,
});
console.log("User returned by profile email:", user)
if (user) {
user.googleId = profile.id;
user.displayName = profile.displayName;
user.image = profile.photos[0].value;
await user.save();
} else {
const newUser = new User({
googleId: profile.id,
displayName: profile.displayName,
email: profile.emails[0].value,
image: profile.photos[0].value,
});
console.log("User before save():", user)
user = await newUser.save();
}
console.log("User before done():", user)
return done(null, user);
} catch (err) {
return done(err, null);
}
},
),
);
// Passport strategy for email/password login combination
// User submitted passwords are compared with stored hashes
passport.use(
new LocalStrategy(
{ usernameField: "email", passwordField: "password" },
async (email, password, done) => {
try {
const user = await User.findOne({ email: email });
if (!user) {
return done(null, false);
} else {
bcrypt.compare(password, user.password, (err, res) => {
if (err) throw err;
if (res === true) {
return done(null, user);
} else {
return done(null, false);
}
});
}
} catch (err) {
console.log(err);
done(err);
}
},
),
);
// On user login, stores user ID in the session store
passport.serializeUser((user, done) => {
console.log("serializing");
console.log(user.id);
process.nextTick(function () {
done(null, user.id);
});
});
// On each subsequent API request, deserializer uses the stored user ID to retrieve user data and stores it under req.user
// Only users with a Google profile have a user.image, so it is optional
passport.deserializeUser((id, done) => {
console.log("deserializing");
process.nextTick(function () {
User.findOne({ _id: id })
.then((user) => {
if (!user) {
return done(null, false);
}
const userInfo = {
id: user._id,
displayName: user.displayName,
email: user.email,
image: user.image || null,
};
done(null, userInfo);
})
.catch((err) => {
console.log(err);
done(err);
});
});
});
// Route handling to follow /notes, /auth, and /local subdirectories
app.use("/api/notes", noteRouter);
app.use("/api/auth", authRouter);
app.use("/api/local", localRouter);
// Database connection
mongoose
.connect(process.env.MONGO_URI)
.then(() => console.log(`Mongodb Connected`))
.catch((error) => console.log(error));
const PORT = process.env.PORT || 4000;
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
当用户被回调函数的 successRedirect 重定向时,意味着用户已正确验证,他们到达主页,并且触发此函数以获取用户信息:
const getGoogleProfile = useCallback(async () => {
try {
const res = await axios.get('/api/auth/login/success', {
withCredentials: true,
});
context.setUserData(res.data.user);
} catch (err) {
console.log(err);
}
}, []);
useEffect(() => {
getGoogleProfile();
}, [getGoogleProfile]);
被触发的函数路由无法访问 req.user,因为 req.user 是“未定义”,并且反序列化器永远不会运行:
router.get("/login/success", (req, res) => {
console.log(req.session)
console.log(req.user)
if (req.user) {
res.status(200).json({
user: req.user,
});
} else {
res.status(403).send("Not authenticated");
}
});
在身份验证过程中,用户信息成功存储在用户数据库中,会话成功存储在mongostore中,并且序列化器运行。在google回调过程中,req.user和req.session都成功记录到控制台,后端可以访问它们。
在 google 回调路由中 successRedirect 期间的某个时刻,req.session 和 req.user 丢失并且不再可访问,即使它们存储在 mongoStore 中。验证过程中不会出现错误消息。
我已经四次检查了 Heroku 和 Netlify 中设置的所有 .env 变量,以及 Google API 控制台中的所有设置。我尝试使用 req.session.save() 和 google 回调中的回调函数手动保存会话。我还尝试了各种不同的 COR、会话、护照策略和 google 路线的配置。
我几乎只是尝试在 Netlify/Heroku 之外的平台上进行托管,或者使用 Express 会话以外的其他平台,因为我觉得我别无选择!
如有任何帮助,我们将不胜感激。
解决了。带有凭据的会话 cookie 已成功发送到浏览器以响应 Google OAuth 2.0 回调,但使用 .heroku.app 作为域。当向 Netlify 上的客户端发出重定向的 get 请求时,由于域不匹配,cookie 被清除,并在其位置发送了一个新的 cookie。开发时,前端和后端都使用localhost作为域名,所以没有冲突。
将会话配置中的域更改为预期接收者,即客户端“.netlify.app”,导致 cookie 中出现错误,“Set-Cookie 被阻止,因为其域属性对于当前主机无效”网址”。我相信由于 Netlify 和 Heroku 位于公共后缀注册表中而无法设置 cookie,但我不确定。
我认为不可能在具有不同域的两个独立托管平台上使用具有后端和前端的某些 OAuth 服务。解决方案是注册一个自定义域名并在 Netlify 和 Heroku 中进行设置。 Netlify 正在处理名称服务器,后端请求通过“api”子域发送到 Heroku。