【问题标题】:How to store a JWT token inside an HTTP only cookie?如何将 JWT 令牌存储在仅限 HTTP 的 cookie 中?
【发布时间】:2017-02-10 03:59:47
【问题描述】:

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

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

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

我了解在这种情况下来回传输令牌的方法。但是,如果您想将该 JWT 存储在客户端 Javascript 无法读取的仅 HTTP 的安全 cookie 中,那么有人会在高层次上解释一下发生的方法吗?

例如:凭据成功时

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

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

【问题讨论】:

    标签: angularjs express cookies passport.js jwt


    【解决方案1】:

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

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

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

    综上所述,cookies 是 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 本身在身份验证范围内使用时也会有其自己的过期概念。

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

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

    TL:DR 人员总结:

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

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

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

    (重点是我的)

    【讨论】:

    • 其实在cookie中存储JWT不是问题,问题是服务器正在对照cookie进行检查。
    • 但关键是我们可以通过生成 XSRF 令牌来阻止 XSRF,但我们不能阻止 XSS,因为用户肯定会在他们的浏览器中拥有一些 chrome/firefox 扩展/插件,并且他们可以轻松读取会话/本地存储。没有办法阻止这种情况。
    【解决方案2】:

    基本上,当用户登录时,我将 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 标头,就极不可能使用刷新令牌泄露任何信息。

    【讨论】:

      猜你喜欢
      • 2020-08-23
      • 2015-09-15
      • 2016-09-13
      • 1970-01-01
      • 2021-01-21
      • 2021-10-16
      • 2021-02-11
      • 2017-12-13
      • 2021-04-30
      相关资源
      最近更新 更多