当令牌无效时,Auth hook 会不断重新渲染

问题描述 投票:0回答:2

我正在 MERN 堆栈中构建一个应用程序,我的身份验证过程遇到了问题,当服务器为

isSessionValid
返回 false 时,我的 DOM 会不断重新渲染: 服务器验证:

const validateSession = async (req, res) => {
    const token = req.cookies.sessionToken;

    if (!token) {
        console.log("No token found in cookies");
        return res.send({ isValidSession: false, message: "No token found in cookies" });
    }

    try {
        const user = await User.findOne({ token });
        if (user && user.expiresAt > new Date().getTime()) {
            return res.send({ isValidSession: true, userId: user.userId });
        } else {
            console.log("Token expired or user not found");
            return res.send({ isValidSession: false });
        }
    } catch (error) {
        console.error("Error validating session:", error);
        return res.status(500).send({ isValidSession: false });
    }
};

这是我的 React useAuth 钩子,它包装了我的整个应用程序:

import React, { createContext, useContext, useState, useEffect, useCallback } from "react";
import axios from "axios";
import { useNavigate } from "react-router-dom";

export const AuthContext = createContext();

export const useAuth = () => useContext(AuthContext);

export const AuthProvider = ({ children }) => {
    const [isAuthenticated, setIsAuthenticated] = useState(false);
    const [userId, setUserId] = useState(null);
    const [loading, setLoading] = useState(true);
    const navigate = useNavigate();

    const validateSession = useCallback(async () => {
        console.log("Validating session...");
        try {
            const response = await axios.get("http://localhost:8000/api/session/validate", { withCredentials: true });
            if (response.data.isValidSession) {
                console.log("Session is valid");
                setIsAuthenticated(true);
                setUserId(response.data.userId);
                localStorage.setItem("tokenExpiry", Date.now() + response.data.expiresIn * 1000);
            } else {
                console.log("Session is not valid");
                setIsAuthenticated(false);
                setUserId(null);
                navigate("/login");
            }
        } catch (error) {
            console.error("Failed to validate session:", error);
            setIsAuthenticated(false);
            setUserId(null);
            navigate("/login");
        } finally {
            setLoading(false);
        }
    }, [navigate]);

    const handleLogout = useCallback(async () => {
        console.log("Logging out...");
        try {
            await axios.post("http://localhost:8000/api/session/logout", {}, { withCredentials: true });
            setIsAuthenticated(false);
            setUserId(null);
            localStorage.removeItem("userId");
            localStorage.removeItem("tokenExpiry");
            navigate("/login");
            console.log("Logout successful");
        } catch (error) {
            console.error("Failed to logout:", error);
        }
    }, [navigate]);

    const handleLogin = (navigate, userId, expiresIn) => {
        setIsAuthenticated(true);
        setUserId(userId);
        localStorage.setItem("tokenExpiry", Date.now() + expiresIn * 1000);
        navigate("/");
    };

    useEffect(() => {
        const tokenExpiry = localStorage.getItem("tokenExpiry");
        const tokenExpiryNumber = Number(tokenExpiry);

        console.log("Token expiry:", tokenExpiry);

        if (!tokenExpiry || isNaN(tokenExpiryNumber) || checkTokenExpiry(tokenExpiryNumber)) {
            console.log("Token is either not present or expired:", tokenExpiry);
            handleLogout();
        } else {
            console.log("Token is valid:", tokenExpiry);
            validateSession();
        }

        const handleBeforeUnload = () => {
            navigator.sendBeacon("/api/session/logout");
        };

        let idleTimeout;

        const resetIdleTimer = () => {
            clearTimeout(idleTimeout);
            idleTimeout = setTimeout(() => {
                handleLogout();
            }, 15 * 60 * 1000); // 15 minutes
        };

        window.addEventListener("beforeunload", handleBeforeUnload);
        window.addEventListener("mousemove", resetIdleTimer);
        window.addEventListener("keypress", resetIdleTimer);

        resetIdleTimer();

        return () => {
            clearTimeout(idleTimeout);
            window.removeEventListener("beforeunload", handleBeforeUnload);
            window.removeEventListener("mousemove", resetIdleTimer);
            window.removeEventListener("keypress", resetIdleTimer);
        };
    }, [handleLogout, validateSession]);

    return (
        <AuthContext.Provider value={{ isAuthenticated, userId, loading, handleLogout, handleLogin }}>{!loading && children}</AuthContext.Provider>
    );
};

const checkTokenExpiry = (expiry) => {
    return expiry < Date.now();
};

根据请求,这是我的登录页面:

import React from "react";
import axios from "axios";
import { useGoogleLogin } from "@react-oauth/google";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../../../../common/hooks/useAuth";
import { LogIn } from "react-feather";

const Login = () => {
    const navigate = useNavigate();
    const { handleLogin } = useAuth();

    const login = useGoogleLogin({
        clientId: process.env.REACT_APP_GOOGLE_CLIENT_ID,
        auto_select: true,
        onSuccess: async (tokenResponse) => {
            try {
                const googleUserResponse = await axios.get("https://www.googleapis.com/oauth2/v3/userinfo", {
                    headers: {
                        Authorization: `Bearer ${tokenResponse.access_token}`,
                    },
                });

                const loginResponse = await axios.post(
                    "http://localhost:8000/api/users/login",
                    {
                        token: tokenResponse.access_token,
                        expiresAt: new Date().getTime() + tokenResponse.expires_in * 1000,
                        email: googleUserResponse.data.email,
                    },
                    { withCredentials: true }
                );

                const userId = loginResponse.data.userId;
                console.log("User ID:", userId);
                localStorage.setItem("userId", userId);
                handleLogin(navigate, userId, tokenResponse.expires_in);
            } catch (error) {
                console.error("Failed to fetch user data or send to backend:", error);
            }
        },
        onError: (error) => {
            console.error("Login Failed:", error);
        },
    });

    return (
        <div className="container">
            <div className="row justify-content-center align">
                <div className="col-8">
                    <div className="card my-5">
                        <div className="card-body shadow">
                            <div className="d-flex justify-content-center mb-1"></div>
                            <h2 className="card-title text-center mb-2">Login Page</h2>
                            <div className="d-flex justify-content-center">
                                <button onClick={() => login()} className="btn btn-primary d-flex align-items-center">
                                    Google
                                    <LogIn size={20} className="ms-1" />
                                </button>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    );
};

export default Login;

这是我在 App.js 和 index.js 中的层次结构

// App.js
import "./App.css";
import Login from "./modules/components/logic/Login";
import JobDetailsScreen from "./pages/job_details/JobDetailsScreen";
import JobApplication from "./pages/job_application/JobApplication";
import Careers from "./pages/careers_page/Careers";
import { Route, Routes } from "react-router-dom";
import ProtectedRoute from "./modules/components/utils/ProtectedRoute";
import Navbar from "./modules/components/card/Navbar";
import Candidate from "./pages/candidate/Candidate";

function App() {
    return (
        <>
            <Navbar />
            <Routes>
                <Route path="/login" element={<Login />} />
                <Route
                    path="/"
                    element={
                        <ProtectedRoute>
                            <Careers />
                        </ProtectedRoute>
                    }
                />
                <Route
                    path="/careers/:jobId"
                    element={
                        <ProtectedRoute>
                            <JobDetailsScreen />
                        </ProtectedRoute>
                    }
                />
                <Route
                    path="/careers/apply/:jobId"
                    element={
                        <ProtectedRoute>
                            <JobApplication />
                        </ProtectedRoute>
                    }
                />
                <Route
                    path="/user-applications/:userId"
                    element={
                        <ProtectedRoute>
                            <Candidate />
                        </ProtectedRoute>
                    }
                />
            </Routes>
        </>
    );
}

export default App;

//index.js
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { GoogleOAuthProvider } from "@react-oauth/google";
import { AuthProvider } from "./common/hooks/useAuth";
import "./index.css";
import App from "./ui/App";
import reportWebVitals from "./reportWebVitals";
import "bootstrap/dist/css/bootstrap.min.css";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
    <>
        <GoogleOAuthProvider clientId={process.env.REACT_APP_CLIENT_ID}>
            <BrowserRouter>
                <AuthProvider>
                    <App />
                </AuthProvider>
            </BrowserRouter>
        </GoogleOAuthProvider>
    </>
);

reportWebVitals();

知道为什么会发生这种情况吗? 如果您希望我提供更多代码以使其更清楚,请 lmk。

javascript reactjs mern
2个回答
1
投票

导致重新渲染的原因是下面的

validateSession
hook

发生的情况是,当调用

validateSession
useEffect
时,它们都会调用到
/login
的导航,这又会重新触发
useEffect
,也再次调用
validateSession
,这将也再次失败,因此再次导航到
/login

useEffect(() => {
  const tokenExpiry = localStorage.getItem("tokenExpiry");
  const tokenExpiryNumber = Number(tokenExpiry);

  console.log("Token expiry:", tokenExpiry);

  if (!tokenExpiry || isNaN(tokenExpiryNumber) || checkTokenExpiry(tokenExpiryNumber)) {
    console.log("Token is either not present or expired:", tokenExpiry);
    handleLogout();
  } else {
    console.log("Token is valid:", tokenExpiry);
    validateSession();
  }

  const handleBeforeUnload = () => {
    navigator.sendBeacon("/api/session/logout");
  };

  let idleTimeout;

  const resetIdleTimer = () => {
    clearTimeout(idleTimeout);
    idleTimeout = setTimeout(() => {
      handleLogout();
    }, 15 * 60 * 1000); // 15 minutes
  };

  window.addEventListener("beforeunload", handleBeforeUnload);
  window.addEventListener("mousemove", resetIdleTimer);
  window.addEventListener("keypress", resetIdleTimer);

  resetIdleTimer();

  return () => {
    clearTimeout(idleTimeout);
    window.removeEventListener("beforeunload", handleBeforeUnload);
    window.removeEventListener("mousemove", resetIdleTimer);
    window.removeEventListener("keypress", resetIdleTimer);
  };
}, [handleLogout, validateSession]);

额外提示:您试图在其中做太多事情

useEffect
,我相信您可以尝试在这里分离一些逻辑。


0
投票

虽然我喜欢其他答案,但我找到了问题的原因,问题是我的

NavBar
组件正在重新渲染所有内容,因为当用户未经身份验证时,导航栏将检查用户是否也经过身份验证,如果不是,它将检查加载是否为真,如果不是,那么它将重新路由到
/login
路线,这导致了无限循环。

解决方案? 不需要

Navbar
检查
user
是否在组件渲染上经过身份验证,只需从
isAuthenticated
钩子检查
useAuth
的更新即可。

© www.soinside.com 2019 - 2024. All rights reserved.