node.js - 在哪里存储访问 token 以及如何跟踪用户(在 Http only cookie 中使用 JWT token )

标签 node.js reactjs authentication cookies jwt

试图了解如何在客户端中获取并保存用户(在 Http only cookie 中使用 JWT token ),以便我可以进行条件渲染。我遇到的困难是如何连续知道用户是否登录,而不必在用户每次更改/刷新页面时向服务器发送请求。 (注意:问题不在于如何在 Http only cookie 中获取 token ,我知道这是通过 withCredentials: true 完成的)

所以我的问题是您如何获取/存储访问 token ,以便每次用户在网站上执行某些操作时,客户端都不必向服务器发出请求。例如,导航栏应该根据用户是否登录进行条件渲染,那么我不想做“询问服务器用户是否有访问 token ,如果没有则检查用户是否有刷新 token ,然后每次用户切换页面时,如果为真,则返回一个新的访问 token ,否则重定向到登录页面。

客户:

UserContext.js

import { createContext } from "react";
export const UserContext = createContext(null);

App.js

const App = () => {
  const [context, setContext] = useState(null);

  return (
    <div className="App">
      <BrowserRouter>
        <UserContext.Provider value={{ context, setContext }}>
          <Navbar />
          <Route path="/" exact component={LandingPage} />
          <Route path="/sign-in" exact component={SignIn} />
          <Route path="/sign-up" exact component={SignUp} />
          <Route path="/profile" exact component={Profile} />
        </UserContext.Provider>
      </BrowserRouter>
    </div>
  );
};

export default App;

配置文件.js

import { GetUser } from "../api/AuthenticateUser";

const Profile = () => {
  const { context, setContext } = useContext(UserContext);

  return (
    <div>
      {context}
      <button onClick={() => GetUser()}>Change context</button>
    </div>
  );
};

export default Profile;

AuthenticateUser.js

import axios from "axios";

export const GetUser = () => {
  try {
    axios
      .get("http://localhost:4000/get-user", {
        withCredentials: true,
      })
      .then((response) => {
        console.log(response);
      });
  } catch (e) {
    console.log(`Axios request failed: ${e}`);
  }
};

服务器:

AuthenticateUser.js

const express = require("express");
const app = express();
require("dotenv").config();
const cors = require("cors");
const mysql = require("mysql");
const jwt = require("jsonwebtoken");
const cookieParser = require("cookie-parser");
// hashing algorithm
const bcrypt = require("bcrypt");
const salt = 10;

// app objects instantiated on creation of the express server
app.use(
  cors({
    origin: ["http://localhost:3000"],
    methods: ["GET", "POST"],
    credentials: true,
  })
);
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());

const db = mysql.createPool({
  host: "localhost",
  user: "root",
  password: "password",
  database: "mysql_db",
});

//create access token
const createAccessToken = (user) => {
  // create new JWT access token
  const accessToken = jwt.sign(
    { id: user.id, email: user.email },
    process.env.ACCESS_TOKEN_SECRET,
    {
      expiresIn: "1h",
    }
  );
  return accessToken;
};

//create refresh token
const createRefreshToken = (user) => {
  // create new JWT access token
  const refreshToken = jwt.sign(
    { id: user.id, email: user.email },
    process.env.REFRESH_TOKEN_SECRET,
    {
      expiresIn: "1m",
    }
  );
  return refreshToken;
};

// verify if user has a valid token, when user wants to access resources
const authenticateAccessToken = (req, res, next) => {
  //check if user has access token
  const accessToken = req.cookies["access-token"];

  // if access token does not exist
  if (!accessToken) {
    return res.sendStatus(401);
  }

  // check if access token is valid
  // use verify function to check if token is valid
  jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
    if (err) return res.sendStatus(403);
    req.user = user;
    return next();
  });
};

app.post("/token", (req, res) => {
  const refreshToken = req.cookies["refresh-token"];
  // check if refresh token exist
  if (!refreshToken) return res.sendStatus(401);

  // verify refresh token
  jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, user) => {
    if (err) return res.sendStatus(401);

    // check for refresh token in database and identify potential user
    sqlFindUser = "SELECT * FROM user_db WHERE refresh_token = ?";
    db.query(sqlFindUser, [refreshToken], (err, user) => {
      // if no user found
      if (user.length === 0) return res.sendStatus(401);

      const accessToken = createAccessToken(user[0]);
      res.cookie("access-token", accessToken, {
        maxAge: 10000*60, //1h
        httpOnly: true, 
      });
      res.send(user[0]);
    });
  });
});

/**
 * Log out functionality which deletes all cookies containing tokens and deletes refresh token from database
 */
app.delete("/logout", (req, res) => {
  const refreshToken = req.cookies["refresh-token"];
  // delete refresh token from database
  const sqlRemoveRefreshToken =
    "UPDATE user_db SET refresh_token = NULL WHERE refresh_token = ?";
  db.query(sqlRemoveRefreshToken, [refreshToken], (err, result) => {
    if (err) return res.sendStatus(401);

    // delete all cookies
    res.clearCookie("access-token");
    res.clearCookie("refresh-token");
    res.end();
  });
});

// handle user sign up
app.post("/sign-up", (req, res) => {
  //request information from frontend
  const { first_name, last_name, email, password } = req.body;

  // hash using bcrypt
  bcrypt.hash(password, salt, (err, hash) => {
    if (err) {
      res.send({ err: err });
    }

    // insert into backend with hashed password
    const sqlInsert =
      "INSERT INTO user_db (first_name, last_name, email, password) VALUES (?,?,?,?)";
    db.query(sqlInsert, [first_name, last_name, email, hash], (err, result) => {
      res.send(err);
    });
  });
});

/*
 * Handel user login
 */
app.post("/sign-in", (req, res) => {
  const { email, password } = req.body;

  sqlSelectAllUsers = "SELECT * FROM user_db WHERE email = ?";
  db.query(sqlSelectAllUsers, [email], (err, user) => {
    if (err) {
      res.send({ err: err });
    }

    if (user && user.length > 0) {
      // given the email check if the password is correct

      bcrypt.compare(password, user[0].password, (err, compareUser) => {
        if (compareUser) {
          //req.session.email = user;
          // create access token
          const accessToken = createAccessToken(user[0]);
          const refreshToken = createRefreshToken(user[0]);
          // create cookie and store it in users browser
          res.cookie("access-token", accessToken, {
            maxAge: 10000*60, //1h
            httpOnly: true, 
          });
          res.cookie("refresh-token", refreshToken, {
            maxAge: 2.63e9, // approx 1 month
            httpOnly: true,
          });

          // update refresh token in database
          const sqlUpdateToken =
            "UPDATE user_db SET refresh_token = ? WHERE email = ?";
          db.query(
            sqlUpdateToken,
            [refreshToken, user[0].email],
            (err, result) => {
              if (err) {
                res.send(err);
              }
              res.sendStatus(200);
            }
          );
        } else {
          res.send({ message: "Wrong email or password" });
        }
      });
    } else {
      res.send({ message: "Wrong email or password" });
    }
  });
});

app.get("/get-user", (req, res) => {
  const accessToken = req.cookies["acceess-token"];
  const refreshToken = req.cookies["refresh-token"];
  //if (!accessToken && !refreshToken) res.sendStatus(401);

  // get user from database using refresh token
  // check for refresh token in database and identify potential user
  sqlFindUser = "SELECT * FROM user_db WHERE refresh_token = ?";
  db.query(sqlFindUser, [refreshToken], (err, user) => {
    console.log(user);
    return res.json(user);
  });
});

app.listen(4000, () => {
  console.log("running on port 4000");
});

如您在上面的客户端代码中所见,我开始尝试使用 useContext。我最初的想法是在 App 组件中使用 useEffect,我在其中调用函数 GetUser(),该函数向“/get-user”发出请求,它将使用 refreshToken 来查找用户(不知道它是否是使用 refreshToken 在 db 中查找用户的错误做法,也许我也应该在 db 中存储访问 token 并使用它在 db 中查找用户?)然后保存诸如 id、名字、姓氏和电子邮件之类的内容,以便它可以必要时显示在导航栏或任何其他组件中。

但是,我不知道这样做是否正确,因为我听说过很多关于使用 localStorge、内存或 sessionStorage 更适合保留 JWT 访问 token 的信息,而您应该将刷新 token 保留在服务器并将其保存在我创建的 mySQL 数据库中,只有在用户丢失访问 token 后才能使用。我应该如何访问我的访问 token 以及如何跟踪登录的用户?每次用户切换页面或刷新页面时,我真的需要向服务器发出请求吗?

我还有一个问题,关于何时应该在服务器中调用“/token”来创建新的访问 token 。我是否应该始终尝试使用访问 token 来执行需要身份验证的操作,例如,如果它在某个时候返回 null ,那么我会向“/token”发出请求,然后重复用户正在尝试的操作做什么?

最佳答案

Do I really need to do a request to the server each time the user switches page or refresh page?

这是最安全的方法。如果您想保持 SPA 的当前安全最佳实践,那么使用仅限 http、安全的同一站点 cookie 是最佳选择。您的页面不会经常刷新,所以这应该不是问题。

My initial idea was to use useEffect in the App component where I make a call to the function GetUser() which makes a request to "/get-user" which will user the refreshToken to find the user

我要做的是首先验证访问 token ,如果它有效,然后从访问 token 中取出 userId(如果你没有它,你可以在手动创建 token 时轻松添加它)并从数据库中读取用户数据。如果访问 token 无效,则向网站返回错误并让用户使用刷新 token 获取新的访问 token 。所以我不会在这里混合职责 - 我不会使用刷新 token 来获取有关已登录用户的信息。

Also I have a question about when I should be calling "/token" in the server to create new access tokens. Should I always try to use the access token to do things that require authentication and if it for example returns null at some point then I make request to "/token" and after that repeat what the user was trying to do?

是的,这就是它通常的实现方式。您使用访问 token 调用 protected 端点。如果 token 已过期或无效,端点最好返回 401 响应。然后您的应用知道它应该使用刷新 token 来获取新的访问 token 。获得新的访问 token 后,您将尝试再次调用 protected 端点。如果您没有设法获得新的访问 token (例如,因为刷新 token 已过期),那么您会要求用户重新登录。

关于node.js - 在哪里存储访问 token 以及如何跟踪用户(在 Http only cookie 中使用 JWT token ),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/69973550/

相关文章:

javascript - node.js csurf 无效的 csrf token

javascript - 检查数组是否包含javascript中的null以外的内容?

javascript - 为什么遍历 ES6 Map 不会在 ReactJS 中创建任何子元素?

security - 使用 url 中的用户名/密码的 Apache 基本身份验证

java - WSO2 身份服务器身份验证管理 API 身份验证失败

mysql - 在 Node.js 中同时生成的发票具有相同的编号。如何实现唯一性?

javascript - 从 Node 确定Docker for Mac RAM限制

javascript - 如何重新初始化所需的模块?

reactjs - React 路由器显示白色空白页

ruby-on-rails - 第一次进入站点根目录时设计身份验证错误