发布于 2026-01-06 1 阅读
0

Node.js 中的 JWT 身份验证 01 JWT 究竟是什么? 02 为什么我们需要 JWT? 03 Node.js 中的 JWT 身份验证

Node.js 中的 JWT 身份验证

01 JWT究竟是什么?

02 为什么我们需要 JWT?

03 Node.js 中的 JWT 身份验证

Hola Amigos!

我最近在学习 JWT 及其在 Node.js 中的应用,现在很高兴能和大家分享我的学习心得。希望你们喜欢阅读。这篇文章我将讨论以下内容:

  1. JSON Web Token 究竟是什么?
  2. 为什么我们需要 JSON Web Token
  3. 在 Node.js 中使用 Express.js 进行 JWT 身份验证

01 JWT究竟是什么?

根据JWT官方网站的信息:

JSON Web Token (JWT) 是一种开放标准 ( RFC 7519 ),它定义了一种紧凑且自包含的方式,用于在各方之间安全地以 JSON 对象的形式传输信息。由于信息经过数字签名,因此可以进行验证并值得信赖。JWT 可以使用密钥(通过 HMAC 算法)或公钥/私钥对(使用 RSA 或 ECDSA 算法)进行签名。

替代文字什么?!

好的!简单来说,JWT 是一种令牌,它允许在同一或不同的 Web 服务器之间安全地传输数据。

但这与传统的按次计费方式有何不同?

替代文字传统的基于会话的用户授权

传统方法中,每当用户使用用户凭据向服务器发送请求时,用户信息都会存储在服务器的会话中,服务器会将会话 ID 作为 cookie 发送出去。这会验证客户端身份,客户端可以将此 cookie 附加到其之后向服务器发送的所有请求中。对于每个请求,服务器都必须查找会话 ID 并验证用户身份,然后才能返回响应。

替代文字JSON Web Tokens (JWT)

在 JWT 协议中,客户端请求访问后,服务器会生成一个与该用户对应的 JWT,其中包含加密的用户信息。因此,服务器无需存储任何用户信息,用户信息存储在客户端。该 JWT 会被发送回客户端,客户端后续的每个请求都会包含此 JWT。浏览器会检查 JWT 的签名,以确定它对应的用户,并将响应发送回客户端。

JWT结构

简洁的 JSON Web Tokens 由三个部分组成,中间用点 ( .) 分隔,分别是:

  • 标题
  • 有效载荷
  • 签名

因此,JWT 通常如下所示。

xxxxx.yyyyy.zzzzz

以下是一个 JWT 示例:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdXRoX2lkIjoiMzIxNjA1MTA1NDEyQUM2QUVCQzQyOTBERUIxMUJENkEiLCJjbGllbnRfaWQiOiIiLCJjc3JmX3Rva2VuIjoiNHJWMGRuWmpJbEdNOFYrNHN3cFZJQkN0M054SjArYlVkVldTdkNDQUJoaz0iLCJpYXQiOjE2MjA4MzQwNjYsInVzZXJfaWQiOiIyYmJlN2QxMC1hYzYxLTQ2NDItODcyMC04OTI1NGEyYzFhYTgiLCJ1c2VyX3R5cGUiOiJndWVzdF9wYXJlbnQiLCJpc19ndWVzdCI6ZmFsc2V9.FNQFIm0_a7ZA5UeMAlQ1pdKS9r6dbe7ryKU42gT5nPc
Enter fullscreen mode Exit fullscreen mode

让我们打开jwt.io调试器,来操作一个示例 JWT 令牌,以下是调试器的屏幕截图。

替代文字

如果你仔细看,你会发现钥匙由三部分组成。

  1. 头部 信息 包含算法所需的信息以及令牌类型。

  2. 有效载荷包含 声明。声明是关于实体(通常是用户)和其他数据的陈述。

    如需了解更多关于索赔类型的信息,您可以参考官方文档:  https://jwt.io/introduction

    您可能已经注意到 iat密钥,它代表 “发行时间”,即该代币的发行日期。这主要用于使代币在一定时间后过期。

  3. 验证 签名部分主要用于服务器验证签名。我们需要添加一个密钥来确保其安全性。

假设客户端试图侵犯令牌并删除一个字符,那么该令牌将立即失效,因为红色和粉色部分与蓝色部分的签名不匹配。

02 为什么我们需要 JWT?

  1. 它存储在客户端。

    替代文字

    假设客户需要访问雅马哈音乐商店服务器,但他只能通过雅马哈的主服务器访问该服务器。在这种情况下,如果我们采用以下方式:

    a) 传统的基于会话的方法中,用户信息存储在服务器端,音乐商店服务器将没有这些信息,用户必须重新登录并验证身份才能访问雅马哈音乐商店。摩托车商店服务器的情况也一样(参见图片)。

    b) 基于 JWT 的方法,由于用户数据存储在客户端,即使经过 JWT 重定向,用户仍然可以通过主服务器向音乐商店服务器或汽车商店服务器发出请求,而不会被强制登出。需要注意的是:在使用 JWT 时,服务器之间必须共享同一个密钥,客户端才能访问这些服务器。

  2. 更紧凑

    与 SAML 相比,JSON 比 XML 更简洁,编码后的文件大小也更小,因此 JWT 比 SAML 更紧凑。这使得 JWT 成为在 HTML 和 HTTP 环境中传递数据的理想选择。

  3. 易用性

    JSON 解析器在大多数编程语言中都很常见,因为它们可以直接映射到对象。这使得使用 JWT 更加容易。

03 Node.js 中的 JWT 身份验证

现在让我们尝试在 Node.js 中构建一个简单的 JWT 身份验证服务。

1. 设置

为了展示 JWT 的跨服务器应用,我将创建两个不同的服务器,一个用于处理所有与身份验证相关的请求,并将其命名为“身份验证服务器”, authServer.js 另一个用于处理所有其他 API 请求,以从服务器获取一些信息,我们也将其命名为“  身份验证服务器”。server.js

authServer.js 将监听端口 5000 和 server.js 端口 4000

首先,我们来安装几个模块。

npm install express jsonwebtoken

注意:我们已 express 在 node 之上安装了一个框架来处理所有与服务器相关的操作,以及 对用户jsonwebtoken 进行签名 jwt ,或者简单地获取用户的 jwt。

安装完成后,我们将在两个文件中调用这些模块,即 authServer.js 和 server.js

const express = require('express');
const jwt = require('jsonwebtoken');

const app = express();

app.use(express.json());
Enter fullscreen mode Exit fullscreen mode

2. 登录时生成 JWT

让我们编写第一个 API 调用,它将是一个POST请求,用于在文件中登录用户authServer.js

app.post('/login', (req, res) => {
  // ...
  // Suppose the user authentication is already done

  const username = req.body.username;
  const user = {name: username};

  const accessToken = generateAccessToken(user);
  res.json({accessToken: accessToken});

});

app.listen(5000);
Enter fullscreen mode Exit fullscreen mode

我们来定义generateAccessToken一个函数,该函数将返回 JWT。

const generateAccessToken = (user) => {
  return jwt.sign(user, process.env.ACCESS_TOKEN_SECRET, {expiresIn: '30s'});
}
Enter fullscreen mode Exit fullscreen mode

以下是定义jwt.sign

jwt.sign(payload, secretOrPrivateKey, [options, callback])

回调函数可以分为两种类型:

  • (异步)回调函数会使用 err JWT 或 进行调用。
  • (同步)以字符串形式返回 JWT。

注意:要使用环境变量,我们需要先对其进行配置,为此我们需要安装另一个名为 的模块 dotenv;我们将使用以下命令安装它: npm install dotenv

运行此命令后,我们需要创建一个 .env 文件并将我们的 ACCESS_TOKEN_SECRET 密钥放在其中,该值必须是无法猜测的。例如:

"0704d2bf835240faffab848079ce73ccf728ffd833e721afd4d7184845b5fc8f00e9f4e2baa87f9d77432f06f0f0384c736d585dacf3f736d8eda3b740c727dea7291542235fe02d75e6ba755307e2546408cffce0b210b4834ea5eff2587859d101bf94aac0f062036662f279ce6f12d84b890a0eaa357b70a01c6922621591"

这可以是任何随机值,您可以通过在节点终端中运行以下脚本来生成它:

require('crypto').randomBytes(64).toString('hex');

替代文字

将密钥放入.env文件后,我们需要在两个服务器文件的顶部添加以下行,以便它可以访问process.env变量。

require('dotenv').config();
Enter fullscreen mode Exit fullscreen mode

3. 从服务器获取数据

让我们向服务器发送一个 GET 请求,从文件中获取与已登录用户对应的数据server.js

const articles = [
  {
    id: 1,
    name: "Atul Kumar",
    title: 'First Article',
  },
  {
    id: 2,
    name: "John Doe",
    title: 'Second Article',
  },
  {
    id: 3,
    name: "Don Joe",
    title: 'Third Article',
  },
];

app.get('/articles', authenticateToken, (req, res) => {
  res.json(articles.filter(article => req.user === article.name));
});
Enter fullscreen mode Exit fullscreen mode

如您所见,我们为该authenticateToken请求使用了自定义中间件/article

以下是其定义authenticateToken

注意:我使用了 ES6 箭头函数,因此您需要在发出 GET 请求之前编写此函数。

const authenticateToken = (req, res, next) => {
    // getting the authorization information
  const authHeader = req.headers['authorization'];
    // In our case It's JWT authantication
  const token = authHeader && authHeader.split(' ')[1];

  if (!token) return res.sendStatus(401); // No token found;

    // verify if there is a user corrosponding to the token found in the 
    // authorization header.
  jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
    if (err) return res.sendStatus(403); // The token is there but it's not valid;
        // if the token is valid, i.e the user is present, then in the request we are 
        // attaching the user name, so that it can be used in other action controllers.
    req.user = user.name;
        // proceeding to the next action controller.
    next();
  })
}
Enter fullscreen mode Exit fullscreen mode

我们为什么要这样做 authHeader.split(' ')[1];

由于 JWT 是一种持有者令牌,req.headers['authorization'];它会返回一个字符串,其值类似于:

"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiQXR1bCBLdW1hciIsImlhdCI6MTYyMTAwOTEzMCwiZXhwIjoxNjIxMDA5MTYwfQ.fxDe0Q2S_G5M0qq1Lo91sz2Od9hBS12226Utq0LJ9jY"
Enter fullscreen mode Exit fullscreen mode

我们只需要字符串中的标记部分。

我们主要是在检查发出GET请求的客户端是否/articles拥有访问权限。具体做法是检查请求中是否附加了有效的令牌。在发出GET请求时,我们需要确保在授权标头中包含 JWT。

但如果我们不这样做呢?

如果我们不这样做,那么响应正文中就会出现“未授权”字样,因为如果您查看代码,就会发现当找不到令牌时会发送 401 状态码。

替代文字

让我们在 Postman 应用上尝试一下我们目前为止所创建的内容。

  1. 让我们尝试使用 GET 请求访问文章。

    1. 没有持有者代币:

      替代文字

      如您所见,我们收到401未授权状态,正如我们之前讨论的那样,这是因为我们根本没有提供令牌(您可以看到令牌字段为空)。

    2. 使用无效的持有者令牌:

      我们随便给一个 JWT 令牌,看看在这种情况下会发生什么。

      替代文字
      这次我们收到的是403 Forbidden 状态码,也就是说我们有令牌,但这个令牌似乎无效。

      但是阿图尔,我的令牌怎么会无效呢?

      嗯,可能有两方面的原因——

      a) 令牌已被篡改,或者您可能只是为令牌输入了一个随机字符串。

      b) 该令牌已过期。

      查看代码,jwt.verify()首先会检查该令牌是否有效。如果有效,则返回用户对象;如果无效,则返回 403 状态码。

      如何访问特定用户的文章?

      为此,我们需要先使用用户登录,以便生成新的 JWT。

  2. 现在让我们使用给定的用户名登录。

    我们将请求/login一个 JSON 对象,该对象具有指定的键username

    替代文字

    我们已成功登录并获取了访问令牌(JWT)。

    现在我们可以在 GET请求中使用此accessToken/articles

    替代文字

    如您所见,我们之所以能获取到这位特定用户的文章,是因为我们使用了包含该用户有效负载信息的 JWT。如果您使用其他用户登录,也可以访问他们的文章。

    注意:我们已将过期时间{expiresIn: '30s'}作为jwt.sign()方法的选项,因此如果您在 30 秒后尝试使用相同的 accessToken 进行访问,则会在响应中收到“Forbidden”错误,因为该令牌已失效。但通常情况下,我们不会将过期时间限制为 30 秒(这只是一个示例)。

替代文字

那么用户是否应该每隔 30 秒重新登录才能访问她的文章?

当然不行,我们需要在应用程序中添加另一种令牌,称为刷新令牌。

4. 刷新令牌

概念很简单:每隔 30 秒,我们将借助用户的刷新令牌为用户生成一个新的访问令牌。

理想情况下,我们需要将刷新令牌存储在缓存或数据库中,以便验证哪些用户需要新的访问令牌。但在这个例子中,我们暂且不讨论将其存储在数据库中的问题;我们只关注概念本身。

那我们就把它记在一个变量里吧;

let refreshTokens = [];
Enter fullscreen mode Exit fullscreen mode

注意:这是不良做法,不应在生产环境中使用,因为每次服务器重启后都会refreshTokens清空该变量。我这样做是为了方便我们专注于概念本身。

在我们的.env文件中,我们将添加一个新的密钥REFRESH_TOKEN_SECRET并为其分配一个加密值,就像我们之前对……所做的那样。ACCESS_TOKEN_SECRET

现在,在我们的/login操作控制器中,我们基本上会将它添加到我们创建的数组refreshToken中。refreshTokens

app.post('/login', (req, res) => {
  // ...
  // Suppose the user authentication is already done

  const username = req.body.username;
  const user = {name: username};

  const accessToken = generateAccessToken(user);
  const refreshToken = jwt.sign(user, process.env.REFRESH_TOKEN_SECRET)
    // pushing the refreshToken generated for this particular user.
  refreshTokens.push(refreshToken);
  res.json({accessToken: accessToken, refreshToken: refreshToken});

});
Enter fullscreen mode Exit fullscreen mode

现在我们需要创建一个新的 POST 请求,为authServer.js文件中指定的刷新令牌生成新的访问令牌。

// generates a new access token with the help of the refresh token;
app.post('/token', (req, res) => {
    // getting the token value from the body
  const refreshToken = req.body.token;
  if (!refreshToken) return res.sendStatus(401);
    // if it doesn't belong to the array we created to store all the refreshTokens
    // then return Unauthorized.
  if (!refreshTokens.includes(refreshToken)) return res.sendStatus(403);

  jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, user) => {
    if (err) return res.sendStatus(403);
        // if the user is found generate a new access token
    const accessToken = generateAccessToken({ name: user.name});
    res.json({ accessToken: accessToken });
  })
});
Enter fullscreen mode Exit fullscreen mode

为什么我们不直接将用户对象传递给generateAccessToken

这是因为我们获取的用户对象中存储了一些额外的信息,以下是我们获取的用户对象:

{ name: 'Atul Kumar', iat: 1621086671 }
Enter fullscreen mode Exit fullscreen mode

问题在于,如果我们使用整个用户对象,jwt.sign()每次都会生成相同的 accessToken,因为我们传递的是完全相同的用户对象。iat

现在让我们检查一下 Postman 上的所有功能是否正常。

  1. 我们将登录并在响应中查找访问令牌和刷新令牌。

    替代文字

  2. 我们将获取该用户的所有文章

    替代文字

  3. 现在,如果我们在 30 秒后使用相同的accessToken发出请求,我们将收到Forbidden 错误

    替代文字

  4. 现在我们将为该用户生成一个新的令牌,我们向服务器发送 POST 请求,并将/token我们在第一步中获得的刷新令牌传递给服务器。

    替代文字

    我们将获得一个新的accessToken

  5. 现在我们将使用这个新生成的 accessToken 再次访问文章。

    替代文字

    我们可以再次访问文章,每次令牌过期后都可以这样做。

替代文字

那么,这是否意味着拥有刷新令牌的用户将永远拥有该应用程序的访问权限?他们可以随时生成新的访问令牌吗?

现在确实如此,但我们需要阻止这种情况发生,方法是使刷新令牌失效。但是,什么时候才是使刷新令牌失效的最佳时机呢?

我们将使该 URL 上的刷新令牌失效/logout。让我们为此发起一个删除请求。

5. 使刷新令牌失效

app.delete('/logout', (req, res) => {
  refreshTokens = refreshTokens.filter(token => token !== req.body.token);
  res.sendStatus(204);
})
Enter fullscreen mode Exit fullscreen mode

这将把刷新令牌作为正文参数,我们希望从缓存存储(或者在本例中,从数组)中释放该令牌。

用户每次登出后,刷新令牌(refreshToken)基本上都会过期(从存储中移除)。用户需要重新登录才能获得新的刷新令牌和访问令牌(accessToken)。

试试看:

替代文字

现在,我们无法再通过调用/tokenAPI 来生成任何新的访问令牌,因为我们在注销请求中传递了refreshToken 。

替代文字

这就是我关于JWT的全部内容!

如果您觉得这篇文章有用,或者有任何建议或想法想要分享,请在下方评论区留言 :)

再见,期待下一篇文章!

atulkumar:5000/logout

文章来源:https://dev.to/atlkr9/jwt-authentication-in-node-js-1dc6