在哪里存储访问令牌以及如何跟踪用户(在 Http only cookie 中使用 JWT 令牌)

Posted

技术标签:

【中文标题】在哪里存储访问令牌以及如何跟踪用户(在 Http only cookie 中使用 JWT 令牌)【英文标题】:Where to store access token and how to keep track of user (using JWT token in Http only cookie) 【发布时间】:2021-12-26 14:38:33 【问题描述】:

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

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

客户:

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;

Profile.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 中存储访问令牌并使用它来在 db 中查找用户?)然后保存诸如 id、名字、姓氏和电子邮件之类的内容,以便它可以如有必要,显示在导航栏或任何其他组件中。

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

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

【问题讨论】:

【参考方案1】:

每次用户切换页面或刷新页面时,我真的需要向服务器发出请求吗?

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

我最初的想法是在 App 组件中使用 useEffect,我在其中调用函数 GetUser(),该函数向“/get-user”发出请求,该请求将使用 refreshToken 来查找用户

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

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

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

【讨论】:

以上是关于在哪里存储访问令牌以及如何跟踪用户(在 Http only cookie 中使用 JWT 令牌)的主要内容,如果未能解决你的问题,请参考以下文章

如何跟踪具有访问令牌的用户是否仍具有有效会话?

在永久存储中跟踪 JWT

我应该在哪里以及如何在passportjs中检查访问令牌的有效性

在哪里存储以及如何在客户端维护来自 cosmos db 的延续令牌

在基于浏览器的应用程序中,我在哪里存储 OAuth 刷新令牌

在客户端的哪里存储刷新令牌?