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

时间:2016-10-01 19:38:49

标签: angularjs express cookies passport.js jwt

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

另一方面,AngularJS使用此令牌,将其存储在会话存储中,并使用auth拦截器每次都将令牌发送回服务器。

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

在这种情况下,我理解来回传递令牌的方法。但是,如果您希望将JWT存储在客户端Javascript无法读取的安全,仅HTTP的cookie中,那么有人会非常友好地解释发生的方法吗?

例如:凭证成功

  1. cookie在服务器上创建,
  2. 与Cookie同时创建JWT
  3. 将JWT存储在名为token等的cookie属性中。
  4. 我正试图在这里获得一个关于它是如何工作的心智模型。如果我的理解是正确的,那么这样做就不再需要auth拦截器了,因为在正确的凭证登录时,服务器会在cookie中进行所有的令牌转移。

3 个答案:

答案 0 :(得分:8)

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

这就是为什么你不再需要使用Angular拦截器,因为浏览器本身可以确保发送cookie

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

总结一下,cookie是HTTP的状态管理机制,有关更多详细信息,请参阅相关的RFC 2617

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

如果您浏览上一篇文章,您会注意到JWT的最终表示形式是由点分隔的三个Base64url编码字符串。这特别令人感兴趣,因为它意味着JWT非常适合在HTTP中使用,包括作为cookie的值。

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

此外,Cookie有他们的到期概念,所以请记住,因为JWT本身在身份验证范围内使用时也会有自己的到期概念。

最后,我想解决一些关于在localStorage / sessionStorage中存储JWT的问题。你是对的,如果你这样做,你必须理解它的含义,例如,与存储相关联的域中的任何Javascript代码都能够读取令牌。但是,仅HTTP的cookie也不是银弹。我会给以下文章写一下:Cookies vs Tokens: The Definitive Guide

它侧重于传统会话标识符cookie与基于令牌的(JWT)身份验证系统之间的差异,名为存储令牌的位置?的部分保证读取,因为它解决了与安全性相关的问题存储。

TL的摘要:DR人员:

  

面向网站的两种最常见的攻击媒介是跨站点   脚本(XSS)和跨站点请求伪造(XSRF或CSRF)。当外部实体能够在您的网站或应用程序中执行代码时,就会发生跨站点脚本攻击。 (...)

     

如果攻击者可以在您的域上执行代码,则您的JWT令牌(本地存储中的 )容易受到攻击。 (...)

     

如果您将JWT与本地存储一起使用,则跨站点请求伪造攻击不是问题。另一方面,如果您的用例要求您将JWT存储在cookie中,则需要防范XSRF。

(重点是我的)

答案 1 :(得分:0)

是的,它不需要任何身份验证拦截器。浏览器会将Cookie发送到为每个请求设置的相同来源。

答案 2 :(得分:0)

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

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
          }
        ),
});

生成并保存的 rand 令牌然后作为 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,在 DOM 中运行恶意脚本无法访问刷新令牌,即使刷新令牌不知何故落入坏人之手,那么它将毫无用处,因为它在到达服务器之前根本不保存任何信息。因此,只要在服务器上设置了正确的 cors 标头,就极不可能使用刷新令牌泄露任何信息。