如何将 JWT 令牌存储在仅限 HTTP 的 cookie 中?

How to store a JWT token inside an HTTP only cookie?

我创建了一个应用程序,它仅使用服务器根据正确的登录凭据发送的 JWT,并针对我的后端 Express.js 服务器上的任何 /api 路由进行授权。

AngularJS,另一方面,获取此令牌,将其存储在会话存储中,并使用身份验证拦截器将令牌发送回服务器。

我最近才明白这种做法有多危险。

我了解这种场景下代币的来回转移方式。但是,有人会在高层次上解释当您想将该 JWT 存储在客户端 Javascript 无法读取的安全、仅 HTTP cookie 中时发生的方法吗?

例如:凭据成功后

  1. cookie 在服务器上创建,
  2. 在创建 cookie 的同时创建一个 JWT
  3. 将 JWT 存储在名为令牌等的 cookie 属性 中。

我试图在这里获得一个关于它如何工作的心智模型。如果我的理解是正确的,那么这样做将不再需要 auth 拦截器,因为在正确的凭据登录后,服务器将完成 cookie 内令牌的所有传输。

处理 cookie 有很多微妙之处,但在高层次上,cookie 是您的网络服务器可以设置的一段数据,然后由用户的网络浏览器存储并发送回服务器只要 cookie 有效且适用于发出的请求,浏览器对同一服务器发出的任何未来请求。

(这就是您不再需要使用 Angular 拦截器的原因,因为是浏览器本身确保发送 cookie)

除了一些特殊标志选项(如仅 HTTP)之外,您还可以在更高级别将 cookie 设置为与给定域和路径相关联。例如,您的服务器可以这样设置一个 cookie,它只会在以后由浏览器发送到在 /api 路径下发出的请求。

总而言之,cookie 是 HTTP 的一种状态管理机制,有关详细信息,请参阅相关的 RFC 2617

相比之下,JWT 只是一些具有 well-know 表示并遵循某些约定的数据。更具体地说,JWT 由 header、有效载荷和签名部分组成,通常建议在大多数 JWT 用例中保持较小的有效载荷大小。有关详细信息,请参阅 Get Started with JSON Web Tokens

如果您阅读上一篇文章,您会注意到 JWT 的最终表示形式是三个由点分隔的 Base64url 编码字符串。这一点特别有趣,因为它意味着 JWT well-suited 在 HTTP 中使用,包括作为 cookie 的值。

要记住的一件事是,根据规范,您只能保证浏览器最多支持每个 cookie 4096 字节的 cookie(以 cookie 的名称、值和长度的总和来衡量)属性)。除非你在令牌中存储大量数据,否则你不应该有问题,但这总是需要考虑的事情。是的,您也可以将一个 JWT 令牌分解为多个 cookie,但事情开始变得更加复杂。

此外,cookie 有过期的概念,所以请记住这一点,因为 JWT 本身在身份验证范围内使用时也会有自己的过期概念。

最后,我只想解决您对在 localStorage/sessionStorage 中存储 JWT 的一些担忧。你是对的,如果你这样做,你必须理解它的含义,例如,与存储关联的域中的任何 Javascript 代码都将能够读取令牌。但是,仅 HTTP cookie 也不是 silver-bullet。我会阅读以下文章:Cookies vs Tokens: The Definitive Guide.

它着重于传统 session 标识符 cookie 与 token-based (JWT) 身份验证系统之间的差异,名为 Where to Store Tokens?[=44= 的部分] 值得一读,因为它涉及存储的安全相关方面。

TL:DR 人的总结:

Two of the most common attack vectors facing websites are Cross Site Scripting (XSS) and Cross Site Request Forgery (XSRF or CSRF). Cross Site Scripting) attacks occur when an outside entity is able to execute code within your website or app. (...)

If an attacker can execute code on your domain, your JWT tokens (in local storage) are vulnerable. (...)

Cross Site Request Forgery attacks are not an issue if you are using JWT with local storage. On the other hand, if your use case requires you to store the JWT in a cookie, you will need to protect against XSRF.

(重点是我的)

基本上,我将 access_token(jwt) 保存在用户登录时存储在数据库中的刷新令牌 object 中。请参阅下面保存的 object 示例;

const newToken = new RefreshToken({
        issuedUtc: moment().unix(), /* Current unix date & time */
        expiresUtc: moment().add(4, "days").unix(), /* Current unix date&time + 4 days */
        token: refreshToken, /* Generate random token */
        user: data.id, /* user id */
        /* Signing the access Token */
        access_token: jwt.sign(
          { sub: data.id, user: userWithoutHash },
          Config.secret,
          {
            issuer: "http://localhost:3000",
            expiresIn: "30m", // Expires in 30 minutes
          }
        ),
});

然后将生成并保存的兰特令牌作为 httpOnly cookie 发送到浏览器;

res.cookie("refreshToken", newToken.token, {
          httpOnly: true,
          sameSite: "strict",
});

由于浏览器为每个请求发送 cookie,剩下的就是在受保护的路由上使用中间件,从 cookie 中检索令牌,通过在数据库中查找来验证它是否存在,检查它是否存在未过期,尝试验证保存在数据库中的刷新令牌的访问令牌,如果过期则签署新的 jwt 并更新数据库中的刷新令牌然后允许用户继续进行受保护的路由,如果有效则简单地允许用户继续到受保护的路线。如果刷新令牌已过期,将用户重定向到登录页面,最后如果没有收到刷新令牌也将用户重定向到登录页面。

var cookie = await getcookie(req); // get the cookie as js object using my custom helper function

/* Check if refresh token was received */

if (cookie.refreshToken) {

  /* Check find the refresh token object in the database */

  var refreshToken = await RefreshToken.findOne({
    token: cookie.refreshToken,
  });

  /* Check if the refresh token is still valid using expiry date */

  if (moment.unix(refreshToken.expiresIn) > moment.now()) {

    /* If the condition is fulfilled try to verify the access token using jwt */

    jwt.verify(refreshToken.access_token, Config.secret, async (err, result) => {

      /* in callback check for error */

      if (err) {

        /* If error this means the access_token is expired, so find and update the user's refresh token with a newly signed access token */

        await RefreshToken.findByIdAndUpdate(refreshToken.id, {
          access_token: jwt.sign(
            { sub: result.id, user: result.user },
            Config.secret,
            {
              issuer: "http://localhost:3000",
              expiresIn: "30m", // Expires in 30 minutes
            }
          ),
        });

        /* Proceed to save the user in a local variable then call next */

        res.locals.user = result.user;
        return next();
      }

      /* If no error proceed by saving the user in a local variable then call next */

      res.locals.user = result.user;
      return next();
    });

  } else {

    /* If the refresh token is expired, then redirect to log in */

    return res.status(401).redirect('/login');
  }
} else {

  /* If no refresh token is provided, then redirect to log in */

  return res.status(401).redirect('/login');
}

这是我自己想出的东西,所以我不能说它是完整的证据,但是由于无法在 DOM 中访问 httpOnly cookie,运行 [=22] 中的恶意脚本=] 无法访问刷新令牌,即使刷新令牌以某种方式落入坏人手中,它也将毫无用处,因为它在到达服务器之前根本不包含任何信息。因此,只要在服务器上设置了正确的 cors header,就不太可能使用刷新令牌泄露任何信息。