服务器端处理JWT令牌的最佳实践

124

(源自这篇文章,因为这确实是一个独立的问题,并不特定于NodeJS等)

我正在实现具有身份验证功能的REST API服务器,已成功实现JWT token处理,以便用户可以通过/login端点使用用户名/密码登录,然后从服务器密钥生成JWT token并返回给客户端。然后,客户端将令牌传递到每个经过身份验证的API请求中的服务器,然后使用服务器密钥验证令牌。

但是,我正在尝试了解确切的最佳实践,以及以何种程度验证令牌才能使系统真正安全。 "验证"令牌需要哪些步骤?仅使用服务器密钥验证签名是否正确是否足够,还是还应该将令牌和/或令牌有效负载与存储在服务器上的某些数据进行交叉检查?

令牌身份验证系统只有与每个请求中传递用户名/密码一样安全,前提是获得令牌与获得用户密码同样困难或更困难。然而,在我看到的示例中,生成令牌所需的唯一信息是用户名和服务器端密钥。这是否意味着假设恶意用户获得服务器密钥的知识,他现在可以代表任何用户生成令牌,从而不仅具有一个给定用户的访问权限(如果获得密码将是事实),但实际上具有所有用户帐户的访问权限?

这就引出了以下问题:

1)JWT token验证是否应限于仅验证令牌本身的签名,并依赖于仅服务器密钥的完整性,还是应该使用单独的验证机制?

  • 在某些情况下,我看到令牌和服务器会话的组合使用,在/login端点成功登录后建立会话。 API请求验证令牌,并将令牌中找到的解码数据与存储在会话中的某些数据进行比较。但是,使用会话意味着使用cookie,从某种意义上讲,它会使令牌基础方法的使用变得毫无意义。它还可能会对某些客户端造成问题。

  • 有人可以想象服务器将当前所有使用的令牌存储在memcache或类似系统中,以确保即使服务器密钥被攻击者泄露从而生成“有效”令牌,也只会接受通过/login端点生成的完全相同的令牌。这是合理的还是多余的/过度的?

2) 如果JWT签名验证是验证令牌的唯一方式,意味着服务器密钥的完整性是破解的关键点,那么应该如何管理服务器密钥?从环境变量中读取并在每个部署堆栈中创建(随机化)一次?定期更新或轮换(如果是这样,如何处理在轮换之前创建但需要在轮换后进行验证的现有有效令牌,也许在任何时候服务器保存当前和上一个密钥就足够了)?还是其他方式?

也许我只是在对服务器密钥被攻击者泄露的风险过度担忧,这当然是所有加密情况下必须解决的更一般问题...


1
有很好的问题。关于第二个问题,我遇到了与任何服务器端保存的秘密密钥相同的问题。如果您正在执行任何类型的哈希匹配或非对称解密,无论是签署jwt还是解密存储在数据库中的cc信息,您都需要一个可由服务器上的代码访问的秘密密钥。那么你到底把它放在哪里?这是我找到的最佳答案:https://pcinetwork.org/forum/index.php?threads/pci-dss-3-0-3-5-2-store-secret-and-private-keys-used-to-encrypt-decrypt-cardholder-data-in-one-or-more-of-the.619/ -- 对于jwt密钥来说,可能是最安全的。 - jbd
JWT令牌中的秘钥是什么?我认为JWT令牌本身就是一个秘密。或者秘钥可能是RSAPrivateKey privateKey吗? - kittu
5
这是一段时间以前的问题,但或许对某些人仍有用。在我的情况下,每个用户都有一个“秘钥”。因此,每次用户登录时,我会生成该秘钥并将其存储到数据库中的用户记录中。我使用该秘钥验证 token。注销时,我清除该值。这会自动使之前创建的其他 token 失效(这正是我需要的)。 - Nelson Rodriguez
@NelsonRodriquez 这正是我的方法,这样你基本上就像处理任何 cookie 一样处理会话,对吗? - Amon
5个回答

53

我也在为我的应用程序使用token。虽然我并不是专家,但我可以分享一些自己的经验和想法。

JWT的关键在于完整性。它提供了一种机制,使您的服务器能够验证提供给它的令牌是真实的,并且是由您的服务器提供的。通过您的秘密生成的签名就是提供此功能的方法。因此,如果您的秘密某种方式泄漏,那么个人就可以生成您的服务器认为是自己的令牌。基于令牌的系统仍然比您的用户名/密码系统更安全,仅仅因为有签名验证。即使这种情况下,如果有人已经知道了您的秘密,那么您的系统还有其他安全问题需要处理,而不仅仅是有人制造假令牌(即使是这种情况,只需更改秘密即可确保使用旧秘密创建的任何令牌现在都无效)。

至于有效负载,签名只会告诉您提供给您的令牌与您服务器发送时完全相同。验证有效载荷内容是否有效或适用于您的应用程序显然取决于您。

针对您的问题:

1.)根据我的有限经验,确实最好使用第二个系统验证您的令牌。仅验证签名意味着令牌是使用您的秘密生成的。将任何已创建的令牌存储在某种数据库(redis、memcache/sql/mongo或其他存储方式)中,是确保只接受您的服务器创建的令牌的绝佳方法。在这种情况下,即使您的秘密泄漏了,也不会太糟糕,因为任何生成的令牌都是无效的。这就是我正在采用的方法——所有生成的令牌都存储在数据库(redis)中,并且在每个请求中,我都会验证该令牌是否在我的数据库中,然后才接受它。这样可以撤销令牌的原因很多,例如一些令牌已经被泄露出去、用户注销、密码更改、秘密更改等。

2.) 这是我没有太多经验的领域,我仍在积极研究,因为我不是安全专业人员。如果您发现任何资源,请随意在此发布!目前,我只使用从磁盘加载的私钥,但显然这远非最佳或最安全的解决方案。


5
这里是第二点的一个好回答: http://security.stackexchange.com/questions/87130/json-web-tokens-how-to-securely-store-the-key - Bossliaw
1
由于令牌可在标头中使用,如果令牌被盗并且恶意用户试图使用该令牌登录(知道用户的电子邮件地址),会发生什么? - kittu
29
如果你把每一个JWT都存储起来的话,那么JWT就没有任何好处,和使用随机会话ID差不多。 - ColinM

51

在应用程序中实现JWT时需要考虑以下几点:

  • 保持JWT的生命周期相对较短,并在服务器上管理其生命周期。如果不这样做,而后又需要在JWT中要求更多信息,则必须支持两个版本,或者等待旧的JWT过期后才能实现更改。如果只查看jwt中的 iat 字段并忽略 exp 字段,可以很容易地在服务器上管理它。

  • 考虑将请求的URL包括在JWT中。例如,如果要在端点/my/test/path使用JWT,请在JWT中包含一个字段,如'url':'/my/test/path',以确保它仅在此路径下使用。如果不这样做,可能会发现人们开始在其他端点上使用JWT,甚至是它们不是为其创建的端点。您还可以考虑包括md5(url),因为在JWT中有一个大的url会使JWT变得更大,而且它们可以变得相当大。

  • 如果在API中实施JWT,则应让每个用例可以配置JWT的到期时间。例如,如果对于10个不同用例的10个端点都需要JWT,请确保可以使每个端点接受在不同时间到期的JWT。如果服务于某个端点的数据非常敏感,这样就可以比其他端点更加安全。

  • 不要仅在一定时间后简单地使JWT过期,而应考虑实现支持以下两种情况的JWT:

    • N次使用-只能在到期之前使用N次
    • 在特定时间后过期(如果有一个仅限一次使用的令牌,您肯定不希望它在未使用时永远存在,对吧?)
  • 所有JWT身份验证失败都应生成一个“错误”响应头,说明为什么JWT身份验证失败。例如,“已过期”,“没有剩余用途”,“已撤销”等。这有助于实施者知道他们的JWT失败的原因。

  • 考虑忽略JWT的“header”部分,因为它们泄露信息并使黑客获得一定的控制权。这主要涉及头部中的alg字段 - 忽略它并假设头部是您想要支持的内容,因为这可以避免黑客尝试使用None算法,该算法会删除签名安全检查。

  • JWT应包括标识符,详细说明哪个应用程序生成了令牌。例如,如果您的JWT由2个不同的客户端mychat和myclassifiedsapp创建,则每个JWT应在JWT中的"iss"字段中包含其项目名称或类似的内容,例如"iss":"mychat"。

  • 不应将JWT记录在日志文件中。 JWT的内容可以记录,但不能记录JWT本身。这确保开发人员或其他人无法从日志文件中获取JWT并对其他用户帐户执行操作。
  • 确保您的JWT实现不允许使用“None”算法,以避免黑客创建未签名的令牌。通过忽略JWT的“header”可以完全避免此类错误。
  • 强烈考虑在JWT中使用iat(发布于)而不是exp(到期)。为什么?因为iat基本上意味着JWT创建的时间,这使您可以根据创建日期在服务器上调整JWT何时过期。如果有人传递一个20年后才到期的exp,那么JWT基本上会永久存在!请注意,如果iat在未来,则会自动使JWT过期,但是允许一些余地(例如10秒),以防客户端的时间略微与服务器的时间不同步。
  • 考虑实现一个端点来从json负载创建JWT,并强制所有实现客户端使用此端点来创建他们的JWT。这确保您可以在一个地方轻松解决有关如何创建JWT的任何安全问题。我们在应用程序中没有立即执行此操作,现在必须逐步推出JWT服务器端安全更新,因为我们的5个不同客户端需要时间来实施。此外,使您的创建端点接受一系列json负载以创建JWT,这将减少客户端发送到此端点的http请求数量。
  • 如果您的JWT将用于也支持会话使用的端点,请确保不要将任何必需的内容放入JWT中以满足请求。如果您确保端点在没有提供JWT时与会话配合工作,则可以轻松完成此操作。
  • 通常情况下,JWT最终包含某种用户ID或组ID,并根据此信息允许访问您系统的部分内容。确保您不允许应用程序中的用户冒充其他用户,特别是如果这提供了对敏感数据的访问权限。为什么?即使您的JWT生成过程仅可由“内部”服务访问,开发人员或其他内部团队也可以生成JWT以访问任何用户的数据,例如某个随机客户公司的CEO。例如,如果您的应用程序提供对客户的财务记录的访问权限,那么通过生成JWT,开发人员可以获取任何公司的财务记录!如果黑客以任何方式进入您的内部网络,他们也可以这样做。
  • 如果您打算允许任何包含JWT的URL被缓存,务必确保将不同用户的权限包含在URL而非JWT中。为什么?因为用户可能会获得他们不应该获得的数据。例如,假设超级用户登录到您的应用程序并请求以下URL:/mysite/userInfo?jwt=XXX,并且此URL被缓存。他们登出后,几分钟后,普通用户登录到您的应用程序。他们将获取缓存的内容-包含有关超级用户的信息!这在客户端上较少发生,在服务器上发生的可能性更大,特别是在使用像Akamai这样的CDN并且您让某些文件存在更长时间的情况下。可以通过在URL中包含相关用户信息,并在服务器上验证这些信息(即使对于缓存请求也要验证)来解决此问题,例如:/mysite/userInfo?id=52&jwt=XXX
  • 如果您的JWT旨在像会话cookie一样使用,并且只应在创建JWT的同一台机器上工作,请考虑向您的JWT添加jti字段。这基本上是一个CSRF令牌,可确保不能将您的JWT从一个用户的浏览器传递到另一个用户的浏览器。

  • 1
    你所提到的 created_by,在 JWT 中已经有一个声明叫做 iss(发行人)。 - Fred
    好的,你说得对 - 我会更新并感谢你! - Brad Parks
    1
    如果你需要将经常更新的数据放入JWT负载的字段中(因此你需要定期刷新令牌),则第一点很有用。我认为短寿命周期的JWT令牌只会让它更加安全。 - Ham

    8

    我不认为自己是一个专家,但我想分享一些关于Jwt的想法。

    • 1: As Akshay said, it's better to have a second system to validate your token.

      a.: The way I handle it : I store the hash generated into a session storage with the expiricy time. To validate a token, it needs to have been issued by the server.

      b.:There is at least one thing that must be checked the signature method used. eg :

      header :
      {
        "alg": "none",
        "typ": "JWT"
      }
      
    一些验证JWT的库会接受未检查哈希值的令牌。这意味着,如果不知道用于签署令牌的盐,黑客可能会授予自己某些权限。一定要确保这种情况不会发生。 https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/ 使用带有会话ID的cookie来验证您的令牌是无用的。如果有人想劫持lambda用户的会话,他只需使用嗅探器(例如:wireshark)。此黑客将同时拥有两个信息。
    对于每个密钥都是相同的。总有一种方法可以知道它。
    我处理它的方式与1.a点相关联:我有一个与随机变量混合的秘密。该秘密对于每个令牌都是唯一的。
    “然而,我正在尝试理解如何以及在多大程度上验证令牌,以创建一个真正安全的系统。”
    如果您想获得最佳安全性,请不要盲目遵循最佳实践。最好的方法是了解您正在做什么(当我看到您的问题时,我认为这很好),然后评估您需要的安全性。如果摩萨德想要访问您的机密数据,他们将始终找到方法。(我喜欢这篇博客文章:https://www.schneier.com/blog/archives/2015/08/mickens_on_secu.html

    每个令牌都有一个独特的密钥是一个好主意,但是如何每次创建一个独特的密钥呢?我正在使用Nimbus JWT库。 - kittu
    2
    可能会使用您用户的哈希密码。 - momokjaaaaa
    1
    如果你不按照其他人的方式进行操作,那么别人就很难找到突破你安全措施的方法。这听起来像是“安全通过混淆”的做法。最佳实践之所以被称为最佳实践,是因为它们以实用的方式减轻了最常见的风险。 - Mnebuerquo
    @Mnebuerquo 我完全同意你的观点,那个写这篇文章的家伙不值得信任;-) - Deblaton Jean-Philippe
    1
    他说得没错,我们不应该盲目地遵循最佳实践。了解为什么这些最佳实践被认为是最好的是很有益的。在每个安全设计决策中,都存在安全性和可用性之间的权衡。理解原因意味着您可以明智地做出这些决策。(尽管如此,请继续遵循最佳实践,因为您的用户需要。) - Mnebuerquo
    @Mnebuerquo 我就是写那篇答案的人,在你的评论后我更新了它。可能是两年前我写那篇答案时需要更多的咖啡;-) - Deblaton Jean-Philippe

    4
    这里有很多好的答案。我将整合一些我认为最相关的答案,并添加一些更多的建议。
    1)JWT令牌验证是否应仅限于验证令牌本身的签名,依赖于服务器密钥的完整性,还是需要配合单独的验证机制?
    不应该,原因与令牌密钥泄露无关。每次用户通过用户名和密码登录时,授权服务器应存储生成的令牌或有关生成的令牌的元数据。将此元数据视为授权记录。给定的用户和应用程序对只能在任何给定时间拥有一个有效的令牌或授权。有用的元数据是与访问令牌关联的用户ID、应用程序ID以及发行访问令牌的时间(这允许撤销现有访问令牌并发放新的访问令牌)。在每个API请求上,验证令牌是否包含正确的元数据。您需要持久化有关每个访问令牌发行时间的信息,以便用户可以在其帐户凭据受到损害时撤销现有的访问令牌,然后重新登录并开始使用新的访问令牌。这将更新数据库的访问令牌发行时间(创建授权时间)。在每个API请求上,检查访问令牌的发行时间是否在创建授权时间之后。

    其他安全措施包括不记录JWT并要求使用安全的签名算法,如SHA256。

    2)如果JWT签名验证是验证令牌的唯一手段,也就是说服务器密钥的完整性是破解点,那么服务器密钥应该如何管理?

    服务器密钥的泄露将允许攻击者为任何用户发布访问令牌,并且在步骤1中存储访问令牌数据并不一定会防止服务器接受这些访问令牌。例如,假设某个用户已被发放访问令牌,然后稍后,攻击者为该用户生成了一个访问令牌。访问令牌的授权时间将是有效的。

    就像Akshay Dhalwala所说,如果您的服务器端秘密已经被泄露,那么您需要处理更大的问题,因为这意味着攻击者已经入侵了您的内部网络、源代码存储库或两者都入侵了。

    然而,减轻受损服务器密钥的损害并避免在源代码中存储密钥的系统需要使用像https://zookeeper.apache.org这样的协调服务进行令牌秘密轮换。使用定时作业每隔几个小时生成一个应用程序秘密(取决于您的访问令牌的有效期),并将更新的秘密推送到Zookeeper。在每个需要知道令牌秘密的应用程序服务器上,配置一个ZK客户端,该客户端在ZK节点值更改时进行更新。存储主要和次要秘密,并在更改令牌秘密时将新令牌秘密设置为主要秘密,旧令牌秘密设置为次要秘密。这样,现有的有效令牌仍将有效,因为它们将根据次要秘密进行验证。当次要秘密被替换为旧的主要秘密时,使用次要秘密发出的所有访问令牌都已过期。

    0

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接