我有一个在 localhost:3000 上运行的 React 应用程序,并且有一个在 localhost:4000 上运行的后端服务器, 如果我在未登录时尝试导航或访问受保护的路线,它不应该允许我看到该页面。
当我登录时,我的后端会在 cookie 中给我一个令牌,它是 httpOnly:true,知道如何以安全的方式正确执行身份验证流程
后端API:
exports.login = async (req, resp) => {
try {
// fetch data
const { email, password } = req.body
// validation for fields required
if (!email || !password) {
return resp.status(200).json({
status: "Failed",
msg: "All fields are required in login !",
})
}
// check if email exists in database
// We also populate additionDetails
const user = await User.findOne({ email: email })
.populate("additionalDetails")
.exec()
console.log("Data fetched about User from Db:", user)
// If user not found with provided email
if (!user) {
return resp.status(200).json({
status: "Failed",
msg: "User with given email dont exists. try again !"
})
}
// user exists check password with hashed password
const checkHashedPassword = await bcrypt.compare(password, user.password)
// password is correct generate Token
if (checkHashedPassword) {
// In token we send user id, email and accountType
const payload = {
id: user._id,
email: user.email,
accountType: user.accountType,
}
const tokenOptions = {
expiresIn: "24h"
}
//Token Expires in 2 hours
const token = jwtToken.sign(payload, process.env.JWT_SECRET, tokenOptions)
// we update the object
// send token and password
user.password = undefined;
user.token = token;
console.log("User Modified", user)
// generate cookie
const cookieOptions = {
// expires in 3days
//expiresIn:"3hr, 3days"
expires: new Date(Date.now() + 3 * 24 * 60 * 1000),
httpOnly: true,
}
resp.cookie("token", token, cookieOptions).
// Req body
status(200).json({
success: "Success",
token,
user,
message: 'User Logged in successfully !',
})
}
else {
return resp.status(200).json({
status: "Failed",
msg: "Password dont match ! Try Again !"
})
}
} catch (error) {
console.log(error);
return resp.status(200).json({
status: "Failed",
message: 'Login Failure, please try again',
errormsg: error
});
}
}
前端:
import { Routes, Route } from "react-router-dom";
import "./App.css";
import Navbar from "./components/core/Navbar";
import { HomePage } from "./pages/HomePage";
import CoursePage from "./pages/CoursePage";
import AboutUsPage from "./pages/AboutUsPage";
import LoginPage from "./pages/LoginPage";
import SignUpPage from "./pages/SignUpPage";
import ScrollToTop from "./ScrollToTop";
import ContactPage from "./pages/ContactPage";
import { Toaster } from "react-hot-toast";
import ProfilePage from "./pages/ProfilePage";
import ProtectedRoute from "./components/protectedRoute/ProtectedRoute";
import Test from "./pages/Test";
import {useSelector} from "react-redux"
function App() {
const toastconfiguration={
position:'top-center',
}
return (
<div className="tw-font-inter tw-bg-richblack-900 tw-h-full tw-relative">
<Toaster position="top-center" toastOptions={toastconfiguration} />
<Navbar />
<ScrollToTop />
<Routes>
<Route path='/' element={<HomePage />} />
<Route path='/courses' element={<CoursePage />} />
<Route path='/about' element={<AboutUsPage />} />
<Route path='/login' element={<LoginPage />} />
<Route path='/signup' element={<SignUpPage />} />
<Route path='/contact' element={<ContactPage />} />
<Route element={<ProtectedRoute />}>
<Route element={<ProfilePage />} path="/profile" />
<Route element={<Test />} path="/test" />
</Route>
</Routes>
</div >
);
}
export default App;
import React from 'react'
import { Outlet,Navigate } from 'react-router-dom'
function ProtectedRoute() {
//* Here add the logic of access the global auth token from redux
let auth={'token':false} // This I have saved globally using redux but for now I want to known the solution
return (
auth.token?<Outlet/>:<Navigate to='/login'/>
)
}
export default ProtectedRoute
我没有找到任何文章来正确理解它,我可以分别发送令牌并将其保存在浏览器本地存储中。但我不知道什么是正确的方法。请帮忙
JWT 中通常包含可识别信息,例如用户的 uuid 作为最基本的示例。一般来说,这是一种相当安全的方式来识别您的用户,而无需放弃 uuid 是什么,并且使得生成可解密为数据库中的实际用户的 uuid 变得极其困难。
您可能需要考虑的一些事情是在用户的“会话”中嵌入无法更改的数据,并在每个请求时验证该数据。包含过期时间也并不罕见。
身份验证的示例可能如下所示:(未经测试:))
async function authenticateUserFromPassword(req, res) {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(401).json({ err: "email and password are required" });
}
const user = await User.findOne({ email })
.populate("additionalDetails")
.exec();
const validPassword =
user && (await bcrypt.compare(password, user.password));
if (!user || !validPassword) {
return res.status(401).json({ err: "invalid login" });
}
const payload = {
id: user.id,
accountType: user.accountType,
iss: "https://api.example.com",
aud: ["https://example.com"],
purpose: "access_token",
};
const token = jwtToken.sign(payload, process.env.JWT_SECRET, {
expiresIn: "24h",
});
res.json({ token });
} catch (error) {
return res.status(500).send();
}
}
async function validateAdminTokenMiddleWare (req, res, next) {
try {
const token = req.headers.authorization.split(" ")[1];
const payload = jwtToken.verify(token, process.env.JWT_SECRET);
if (payload.purpose !== "access_token") {
return res.status(401).json({ err: "invalid token" });
}
if (!payload.accountType === "admin") {
return res.status(401).json({ err: "invalid token" });
}
if (payload.iss !== "https://api.example.com") {
return res.status(401).json({ err: "invalid token" });
}
if (!payload.aud.includes("https://example.com")) {
return res.status(401).json({ err: "invalid token" });
}
const user = await User.findById(payload.id)
.populate("additionalDetails")
.exec();
req.user = user;
next();
} catch (error) {
return res.status(401).json({ err: "invalid token" });
}
}
async function validateUserTokenMiddleWare (req, res, next) {
try {
const token = req.headers.authorization.split(" ")[1];
const payload = jwtToken.verify(token, process.env.JWT_SECRET);
if (payload.purpose !== "access_token") {
return res.status(401).json({ err: "invalid token" });
}
if (!payload.accountType === "user") {
return res.status(401).json({ err: "invalid token" });
}
if (payload.iss !== "https://api.example.com") {
return res.status(401).json({ err: "invalid token" });
}
if (!payload.aud.includes("https://example.com")) {
return res.status(401).json({ err: "invalid token" });
}
const user = await User.findById(payload.id)
.populate("additionalDetails")
.exec();
req.user = user;
next();
} catch (error) {
return res.status(401).json({ err: "invalid token" });
}
}
我们使用不同的中间件来相应地处理不同的用户类型。
在前端,您需要将令牌保存到本地存储或等效存储,并将令牌附加到所有需要身份验证的请求的
Authorization
标头。
需要注意的一些事项:最佳做法是始终检查 iss 和 uad 声明。这有助于减轻攻击向量,其中一个资源服务器将获得为其指定的真正访问令牌,然后使用它来访问另一台资源服务器上的资源,而原始服务器通常无法使用这些资源。
我删除了cookie,你不需要它。当您登录时,服务器仅响应 200 和令牌。前端应该从那里管理它。
您不需要使用令牌更新用户对象,事实上最好不要将这些令牌存储在数据库中。
最后,我将有一个 fetch 的包装器。每当遇到 401 时,请删除旧令牌,然后重定向到登录屏幕,当然,除非您已经在登录屏幕上。