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

OAuth在本地及Chrome生产环境正常,Firefox/Safari无法使用

OAuth登录跨浏览器/移动端失效问题

我是Web开发新手,还请多多包涵!我将React应用部署在Netlify(域名:famtivity.fun),后端托管在Heroku。

最初Google/GitHub的OAuth仅在localhost环境正常工作。部署后,OAuth登录重定向回个人资料页时无法完成用户授权,但用户信息已成功写入数据库。在后端express session上方添加app.set("trust proxy", 1)后,Chrome浏览器恢复正常,但移动端和Firefox仍无法正常登录:用户被重定向到个人资料页,但未处于登录状态,不过用户信息依然能写入Heroku的JAWSDB数据库。

仓库链接:

  • 前端:https://github.com/Lindsay-Renna/lindsay-renna-capstone
  • 后端:https://github.com/Lindsay-Renna/lindsay-renna-capstone-api

后端配置代码

const app = express();
app.use(
    cors({
        origin: process.env.CLIENT_URL,
        credentials: true,
    })
);
app.use(express.json());
app.use(helmet());

app.set("trust proxy", 1);
app.use(
    expressSession({
        secret: process.env.SESSION_SECRET,
        resave: false,
        saveUninitialized: true,
        cookie: {
            secure: process.env.NODE_ENV === "production",
            sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
        },
    })
);

// =========== Passport Config ============

// Initialize Passport middleware
app.use(passport.initialize());
app.use(passport.session());

// Initialize GitHub strategy middleware
passport.use(
    new GitHubStrategy(
        {
            clientID: process.env.GITHUB_CLIENT_ID,
            clientSecret: process.env.GITHUB_CLIENT_SECRET,
            callbackURL: process.env.GITHUB_CALLBACK_URL,
        },
        (_accessToken, _refreshToken, profile, done) => {
            console.log("GitHub profile:", profile);

            // Check for user in DB
            knex("users")
                .select("id")
                .where({ github_id: profile.id })
                .then((user) => {
                    if (user.length) {
                        // If user is found, pass user object to serialize function
                        done(null, user[0]);
                    } else {
                        // If user isn't found, create a record
                        knex("users")
                            .insert({
                                github_id: profile.id,
                                google_id: null,
                                avatar_url: profile._json.avatar_url,
                                username: profile.username,
                            })
                            .then((userId) => {
                                // Pass the user object to serialize function
                                done(null, { id: userId[0] });
                            })
                            .catch((err) => {
                                console.log("Error creating a user", err);
                            });
                    }
                })
                .catch((err) => {
                    console.log("Error fetching a user", err);
                });
        }
    )
);

// Initialize Google strategy middleware
passport.use(
    new GoogleStrategy(
        {
            clientID: process.env.GOOGLE_CLIENT_ID,
            clientSecret: process.env.GOOGLE_CLIENT_SECRET,
            callbackURL: process.env.GOOGLE_CALLBACK_URL,
        },
        (_accessToken, _refreshToken, profile, done) => {
            knex("users")
                .select("id")
                .where({ google_id: profile.id })
                .then((user) => {
                    if (user.length) {
                        done(null, user[0]);
                    } else {
                        knex("users")
                            .insert({
                                google_id: profile.id,
                                github_id: null,
                                avatar_url: profile.photos[0].value,
                                username: profile.displayName,
                            })
                            .then((userId) => {
                                done(null, { id: userId[0] });
                            })
                            .catch((err) => {
                                console.log("Error creating a user", err);
                            });
                    }
                })
                .catch((err) => {
                    console.log("Error fetching a user", err);
                });
        }
    )
);

passport.serializeUser((user, done) => {
    console.log("serializeUser (user object):", user);

    done(null, user.id);
});

passport.deserializeUser((userId, done) => {
    console.log("deserializeUser (user id):", userId);

    // Query user information from the database for currently authenticated user
    knex("users")
        .where({ id: userId })
        .then((user) => {
            console.log("req.user:", user[0]);
            done(null, user[0]);
        })
        .catch((err) => {
            console.log("Error finding user", err);
        });
});

认证路由代码

// Github authentication routes
router.get("/github", passport.authenticate("github"));
router.get(
    "/github/callback",
    passport.authenticate("github", {
        failureRedirect: `${process.env.CLIENT_URL}/auth-fail`,
    }),
    (req, res) => {
        res.redirect(`${process.env.CLIENT_URL}/profile`);
    }
);

// Google authentication routes
router.get("/google", passport.authenticate("google", { scope: ["profile"] }));
router.get(
    "/google/callback",
    passport.authenticate("google", {
        failureRedirect: `${process.env.CLIENT_URL}/auth-fail`,
    }),
    (req, res) => {
        // Successful authentication
        res.redirect(`${process.env.CLIENT_URL}/profile`);
    }
);

// User profile endpoint that requires authentication
router.get("/profile", (req, res) => {
    // If `req.user` isn't found send back a 401 Unauthorized response
    if (req.user === undefined)
        return res.status(401).json({ message: "Unauthorized 😒" });

    // If user is currently authenticated, send back user info
    res.status(200).json(req.user);
});

// Logout endpoint
router.get("/logout", (req, res) => {
    req.logout((error) => {
        if (error) {
            return res.status(500).json({
                message: "Server error, please try again later",
                error: error,
            });
        }
        // Redirect the user back to client-side application
        res.redirect(process.env.CLIENT_URL);
    });
});

前端个人资料页认证检查代码

import { useState, useEffect } from "react";
const SERVER_URL = import.meta.env.VITE_APP_SERVER_URL;
import axios from "axios";
import "./ProfilePage.scss";

const ProfilePage = ({ isLoggedIn, setIsLoggedIn }) => {
    const [movies, setMovies] = useState([]);
    const [isAuthenticating, setIsAuthenticating] = useState(true);

    useEffect(() => {
        authenticateUser();
    }, []);

    const [profileData, setProfileData] = useState(null);

    const authenticateUser = async () => {
        try {
            const res = await axios.get(`${SERVER_URL}/auth/profile`, {
                withCredentials: true,
            });
            setIsAuthenticating(false);
            setIsLoggedIn(true);
            console.log(res.data);
            setProfileData(res.data);
            localStorage.setItem("user_id", res.data.id);
            getWatchedList(res.data.id);
        } catch (err) {
            if (err.response && err.response.status == 401) {
                setIsAuthenticating(false);
                setIsLoggedIn(false);
            } else {
                console.log("Error authenticating", err);
            }
        }
    };

    const getWatchedList = async (id) => {
        try {
            const res = await axios.get(`${SERVER_URL}/user/${id}/watched-list`);
            const movies = res.data.sort((a, b) =>
                a.movie_name.localeCompare(b.movie_name)
            );
            setMovies(movies);
        } catch (err) {
            console.log(err);
        }
    };

    const handleRemoveMovie = (id) => {
        try {
            axios.delete(`${SERVER_URL}/user/${id}`);
        } catch (error) {
            console.log(err);
        }
        setMovies(movies.filter((movie) => movie.id !== id));
    };

    return (
        <section className="profile-page">
            {isLoggedIn ? (
                profileData && (
                    <div className="profile-page__profile">
                        <h2 className="profile-page__header">
                            Hello, {profileData.username}
                        </h2>
                        <div className="profile-page__info">
                            <img
                                className="profile-page__avatar"
                                src={profileData.avatar_url}
                                alt={`${profileData.username} avatar`}
                            />
                        </div>
                        <div className="profile-page__watch-list">
                            <DynamicTable
                                movies={movies}
                                handleRemoveMovie={handleRemoveMovie}
                            />
                        </div>
                        <div className="profile-page__logout-wrapper">
                            <LogoutButton setIsLoggedIn={setIsLoggedIn} />
                        </div>
                    </div>
                )
            ) : (
                <div className="login-request">
                    <div className="login-request__container">
                        <h2 className="login-request__header">
                            Please login to access your profile
                        </h2>
                        <LoginButton />
                    </div>
                </div>
            )}
        </section>
    );
};

export default ProfilePage;

前端登录按钮代码

import "./LoginButton.scss";

const SERVER_URL = import.meta.env.VITE_APP_SERVER_URL;

const LoginButton = () => {
    return (
        <div className="sign-on-partners">
            <a className="login-button" href={`${SERVER_URL}/auth/google`}>
                <div className="login-button__wrapper">
                    <img
                        src="/icons/google.svg"
                        alt="google logo"
                        height="32"
                        viewBox="0 0 16 16"
                        version="1.1"
                        width="32"
                    />
                    <span className="login-button__text">Login with Google</span>
                </div>
            </a>
            <a className="login-button" href={`${SERVER_URL}/auth/github`}>
                <div className="login-button__wrapper">
                    <svg height="32" viewBox="0 0 16 16" version="1.1" width="32">
                        <path
                            fillRule="evenodd"
                            d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"
                        ></path>
                    </svg>
                    <span className="login-button__text">Login with GitHub</span>
                </div>
            </a>
        </div>
    );
};

export default LoginButton;

已尝试的解决方案

我曾调整Cookie设置为以下配置,但问题仍未解决:

app.set('trust proxy', 1);
app.use(session({
    secret: "secret",
    resave: false,
    saveUninitialized: false,
    proxy: true,
    cookie: { 
        secure: "auto",
        sameSite: "none",
    },
}))

内容的提问来源于stack exchange,提问作者Lindsay Renna

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 06:55:53