Node中使用带有 JWT 的 Cookie 存储
原文 https://dev.to/franciscomendes10866/using-cookies-with-jwt-in-node-js-8fn
基思·欧文 •
根据我的研究,将身份验证令牌存储在localStorage其中sessionStorage是不安全的,因为可以在 XSS 攻击中从浏览器存储中检索令牌。设置了标志的 CookiehttpOnly无法被客户端 JS 访问,因此不会受到 XSS 攻击。
在了解了这一点后,我尝试实现一个Authorization: Bearer XXXXXXXXX请求标头,但将令牌安全地存储在 cookie 中。然后我意识到如果我无法使用客户端 JS 访问它,我将无法将令牌从 cookie 复制到请求标头(httpOnly记得吗?)
因此,我得出结论,将令牌保存在 httpOnly cookie 中并将其作为请求 cookie 发送到服务器是使用 JWT 的唯一安全方式。
———
将 JWT 保存在 cookie 中的最大区别在于,在发出 http 请求时,cookie 将随请求一起发送。但是,如果您将 JWT 存储在本地存储中,则必须在每个 http 请求中显式发送它。
———–
我非常有信心这对 CSRF 攻击是开放的。攻击者只需要托管另一个站点 B,然后拥有 cookie 的用户最终会将加密的令牌发送到主站点 A,他们可以在其中 /login 然后 /protected。建议结合使用 SameSite(停止尊重相同站点的浏览器的 CSRF)和同步器令牌模式(停止跨站点同源攻击)。两者都应该使用,因为 SameSite 仍然容易受到跨站点同源的攻击。由于无法应用 SameSite,因此使用 GET 也存在重大缺陷。您还提到,因为使用 Cookie 与 Localstorage 的工作量较少 – 并且您使用 HttpOnly 标志来防止 XSS – 很好 – 但应该明确 Localstorage 绝不是 JWT 的选择,不是因为它只是更多的工作,而是 XSS 可攻击。
———–
在本文中分享的设置可能会遭受 CSRF 攻击🧐。但是,本文的目的不是提供一个完全安全的解决方案,而是创建一个简单的身份验证和授权策略,可以在小型个人项目中实施(为了好玩)。
身份验证和授权不是我喜欢讨论的领域,因为可以实现的策略有上千种。但是关于 Cookies vs Local Storage 的讨论,在我看来,让魔鬼来选择吧,各有优缺点,只能由程序员来选择承担哪些风险。这些天,老实说,我无动于衷。
虽然 JWT 是一种非常流行的认证方式,受到很多人的喜爱。大多数人最终将其存储在本地存储中。我不会在这里争论什么是在前端存储 jwt 的最佳方式,这不是我的意图。
如果您已经阅读了我创建的这篇关于如何使用 JWT 创建简单的身份验证和授权系统的文章,您一定已经注意到,当从登录路由发出 http 请求时,我会发送 jwt 作为响应。也就是说,这个想法是将其保存在本地存储中。
但是,还有其他方法可以将 jwt 发送到前端,今天我将教你如何将 jwt 存储在 cookie 中。
为什么要使用 cookie?
有时我有点懒惰,因此我不想在向 Api 发出请求时不断地在标头中发送 jwt。这就是 cookie 的用武之地,您可以在每次发出 http 请求时发送它们而无需担心。
另一个原因是,如果您使用 localstorage,在前端,您必须确保在用户注销时从 localstorage 中删除 jwt。使用 cookie 时,您只需要 api 中的路由来发出 http 请求以删除您在前端拥有的 cookie。
首选使用 cookie 的原因有很多,这里我给出了一些在项目细化过程中可能出现的表面上的小例子。
现在我们有了一个大致的想法,让我们来编码吧!
让我们编码
首先,我们将安装以下依赖项:
npm install express jsonwebtoken cookie-parser
现在只需创建一个简单的 Api:
const express = require("express");
const app = express();
app.get("/", (req, res) => {
return res.json({ message: "Hello World 🇵🇹 🤘" });
});
const start = (port) => {
try {
app.listen(port, () => {
console.log(`Api up and running at: http://localhost:${port}`);
});
} catch (error) {
console.error(error);
process.exit();
}
};
start(3333);
正如您可能已经猜到的那样,我们需要一些能够在我们的 Api 中使用 cookie 的东西,这就是cookie-parser 的用武之地。
首先我们将导入它并在我们的中间件中注册它。
const express = require("express");
const cookieParser = require("cookie-parser");
const app = express();
app.use(cookieParser());
//Hidden for simplicity
现在我们准备开始在我们的 Api 中创建一些路由。
我们要创建的第一个路由是登录路由。首先,我们将创建我们的 jwt,然后将其存储在一个名为“access_token”的 cookie 中。cookie 会有一些选项,例如 httpOnly(在应用程序开发过程中使用)和 secure(在生产环境中使用,带有 https)。
然后我们会发送一个回复说我们已经成功登录了。
app.get("/login", (req, res) => {
const token = jwt.sign({ id: 7, role: "captain" }, "YOUR_SECRET_KEY");
return res
.cookie("access_token", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
})
.status(200)
.json({ message: "Logged in successfully 😊 👌" });
});
现在登录完成,让我们检查我们是否在客户端中收到了带有 jwt 的 cookie,在这种情况下我使用了Insomnia。
现在完成身份验证,让我们进行授权。为此,我们必须创建一个中间件来检查我们是否有 cookie。
const authorization = (req, res, next) => {
// Logic goes here
};
现在我们必须检查我们是否有名为“access_token”的cookie,如果没有,那么我们将禁止访问控制器。
const authorization = (req, res, next) => {
const token = req.cookies.access_token;
if (!token) {
return res.sendStatus(403);
}
// Even more logic goes here
};
如果我们有 cookie,我们将验证令牌以获取数据。但是,如果发生错误,我们将禁止访问控制器。
const authorization = (req, res, next) => {
const token = req.cookies.access_token;
if (!token) {
return res.sendStatus(403);
}
try {
const data = jwt.verify(token, "YOUR_SECRET_KEY");
// Almost done
} catch {
return res.sendStatus(403);
}
};
现在是时候在请求对象中声明新属性,以便我们更轻松地访问令牌的数据。
为此,我们将创建req.userId并分配令牌中 id 的值。我们还将创建req.userRole并分配令牌中存在的角色的值。然后只需授予对控制器的访问权限。
const authorization = (req, res, next) => {
const token = req.cookies.access_token;
if (!token) {
return res.sendStatus(403);
}
try {
const data = jwt.verify(token, "YOUR_SECRET_KEY");
req.userId = data.id;
req.userRole = data.role;
return next();
} catch {
return res.sendStatus(403);
}
};
现在我们要创建一个新的路由,这次我们要创建注销的路由。基本上,我们将从 cookie 中删除值。也就是说,我们将删除 jwt。
但是,我们想将授权中间件添加到我们的新路由中。这是因为如果用户有 cookie,我们想注销。如果用户有 cookie,我们将删除它的值并发送一条消息说用户已成功注销。
app.get("/logout", authorization, (req, res) => {
return res
.clearCookie("access_token")
.status(200)
.json({ message: "Successfully logged out 😏 🍀" });
});
所以现在让我们测试我们是否可以注销。目的是验证第一次注销时,我们会有一条消息说它是成功的。但是当我们在没有cookie的情况下再次测试时,我们必须有一个错误说它是被禁止的。
现在我们只需要创建最后一个路由,以便我们可以从 jwt 获取数据。只有当我们可以访问 cookie 中的 jwt 时,才能访问此路由。如果我们不这样做,我们将得到一个错误。现在我们将能够使用我们添加到请求中的新属性。
app.get("/protected", authorization, (req, res) => {
return res.json({ user: { id: req.userId, role: req.userRole } });
});
如果我们在我们最喜欢的客户端上测试它。我们将首先测试整个工作流程。遵循以下几点:
- 登录获取cookie;
- 访问受保护的路由查看jwt数据;
- 注销以清除cookie;
- 再次访问受保护的路由,但这次我们预计会出错。
我在这里留下一个 gif 来展示最终结果应该如何预期:
最终代码必须如下:
const express = require("express");
const cookieParser = require("cookie-parser");
const jwt = require("jsonwebtoken");
const app = express();
app.use(cookieParser());
const authorization = (req, res, next) => {
const token = req.cookies.access_token;
if (!token) {
return res.sendStatus(403);
}
try {
const data = jwt.verify(token, "YOUR_SECRET_KEY");
req.userId = data.id;
req.userRole = data.role;
return next();
} catch {
return res.sendStatus(403);
}
};
app.get("/", (req, res) => {
return res.json({ message: "Hello World 🇵🇹 🤘" });
});
app.get("/login", (req, res) => {
const token = jwt.sign({ id: 7, role: "captain" }, "YOUR_SECRET_KEY");
return res
.cookie("access_token", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
})
.status(200)
.json({ message: "Logged in successfully 😊 👌" });
});
app.get("/protected", authorization, (req, res) => {
return res.json({ user: { id: req.userId, role: req.userRole } });
});
app.get("/logout", authorization, (req, res) => {
return res
.clearCookie("access_token")
.status(200)
.json({ message: "Successfully logged out 😏 🍀" });
});
const start = (port) => {
try {
app.listen(port, () => {
console.log(`Api up and running at: http://localhost:${port}`);
});
} catch (error) {
console.error(error);
process.exit();
}
};
start(3333);
最后的笔记
显然,这个例子很简单,我不会不推荐阅读更多关于这个主题的内容。但我希望我能帮助解决您的任何疑问。
那你呢?
您是否使用或阅读过此身份验证策略?