在哪里存储访问令牌以及如何跟踪用户(在 Http only cookie 中使用 JWT 令牌)
Where to store access token and how to keep track of user (using JWT token in Http only cookie)
试图了解如何在客户端中获取并保存用户(在 Http only cookie 中使用 JWT 令牌),以便我可以进行条件渲染。我遇到的困难是如何连续知道用户是否登录,而不必在每次用户 changes/refresh 页面时向服务器发送请求。 (注意:问题不是我如何在 Http only cookie 中获取令牌,我知道这是通过 withCredentials: true
完成的)
所以我的问题是您如何 get/store 访问令牌,以便每次用户在网站上执行某些操作时,客户端都不必向服务器发出请求。例如,导航栏应该根据用户是否登录进行条件渲染,那么我不想做“询问服务器用户是否有访问令牌,如果没有则检查用户是否有刷新令牌,然后return 每次用户切换页面时,如果为 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”来创建新的访问令牌。我是否应该始终尝试使用访问令牌来做需要身份验证的事情,如果它例如 returns 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
我要做的是首先验证访问令牌,如果它有效,然后从访问令牌中取出 userId(如果你没有它,你可以在创建令牌时轻松添加它手动)并从数据库中读取用户数据。如果访问令牌无效,则向网站发送 return 错误,让用户使用刷新令牌获取新的访问令牌。所以我不会在这里混合职责 - 我不会使用刷新令牌来获取有关登录用户的信息。
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?
是的,通常就是这样实现的。您使用访问令牌调用受保护的端点。如果令牌已过期或无效,端点 returned 401 响应最好。然后您的应用知道它应该使用刷新令牌来获取新的访问令牌。获得新的访问令牌后,您将尝试再次调用受保护的端点。如果您没有设法获得新的访问令牌(例如,因为刷新令牌已过期),那么您会要求用户重新登录。
试图了解如何在客户端中获取并保存用户(在 Http only cookie 中使用 JWT 令牌),以便我可以进行条件渲染。我遇到的困难是如何连续知道用户是否登录,而不必在每次用户 changes/refresh 页面时向服务器发送请求。 (注意:问题不是我如何在 Http only cookie 中获取令牌,我知道这是通过 withCredentials: true
完成的)
所以我的问题是您如何 get/store 访问令牌,以便每次用户在网站上执行某些操作时,客户端都不必向服务器发出请求。例如,导航栏应该根据用户是否登录进行条件渲染,那么我不想做“询问服务器用户是否有访问令牌,如果没有则检查用户是否有刷新令牌,然后return 每次用户切换页面时,如果为 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”来创建新的访问令牌。我是否应该始终尝试使用访问令牌来做需要身份验证的事情,如果它例如 returns 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
我要做的是首先验证访问令牌,如果它有效,然后从访问令牌中取出 userId(如果你没有它,你可以在创建令牌时轻松添加它手动)并从数据库中读取用户数据。如果访问令牌无效,则向网站发送 return 错误,让用户使用刷新令牌获取新的访问令牌。所以我不会在这里混合职责 - 我不会使用刷新令牌来获取有关登录用户的信息。
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?
是的,通常就是这样实现的。您使用访问令牌调用受保护的端点。如果令牌已过期或无效,端点 returned 401 响应最好。然后您的应用知道它应该使用刷新令牌来获取新的访问令牌。获得新的访问令牌后,您将尝试再次调用受保护的端点。如果您没有设法获得新的访问令牌(例如,因为刷新令牌已过期),那么您会要求用户重新登录。