我正在 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。
导致重新渲染的原因是下面的
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
,我相信您可以尝试在这里分离一些逻辑。
虽然我喜欢其他答案,但我找到了问题的原因,问题是我的
NavBar
组件正在重新渲染所有内容,因为当用户未经身份验证时,导航栏将检查用户是否也经过身份验证,如果不是,它将检查加载是否为真,如果不是,那么它将重新路由到/login
路线,这导致了无限循环。
解决方案? 不需要
Navbar
检查 user
是否在组件渲染上经过身份验证,只需从 isAuthenticated
钩子检查 useAuth
的更新即可。