试图了解如何在客户端中获取并保存用户(在 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/