React 和 NodeJs 中使用 Jwt 令牌的身份验证流程

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

我有一个在 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

我没有找到任何文章来正确理解它,我可以分别发送令牌并将其保存在浏览器本地存储中。但我不知道什么是正确的方法。请帮忙

reactjs node.js authentication oauth jwt
1个回答
0
投票

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 时,请删除旧令牌,然后重定向到登录屏幕,当然,除非您已经在登录屏幕上。

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