【发布时间】: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”发出请求,然后重复用户尝试做的事情?
【问题讨论】:
标签: node.js reactjs authentication cookies jwt