前端客户端JWTs处理指南(2019年)

2020-06-27 21:54:58

JWT(JSON Web Token,发音为JSON Web Token)正在成为一种流行的身份验证处理方式。这篇文章旨在揭开什么是JWT的神秘面纱,讨论它的优缺点,并介绍在客户端实现JWT的最佳实践,同时考虑到安全性。虽然我们已经使用GraphQL客户端进行了示例,但是这些概念适用于任何前端客户端。

出于身份验证的目的,JWT是由服务器颁发的令牌。令牌有一个JSON有效负载,其中包含特定于用户的信息。此令牌可由客户端在与API对话时使用(通过将其作为HTTP标头一起发送),以便API可以识别令牌所代表的用户,并采取特定于用户的操作。

但是客户机不能只创建一个随机的JSON有效负载来模拟用户吗?

问得好!这就是JWT还包含签名的原因。此签名是由颁发令牌的服务器(假设您的登录端点)创建的,接收此令牌的任何其他服务器都可以独立验证签名,以确保JSON有效负载未被篡改,并且具有由合法来源发布的信息。

是!。如果JWT被盗,窃贼可以继续使用JWT。接受JWT的API进行独立的验证,而不依赖于JWT源,因此API服务器无法知道这是否是被盗的令牌!这就是JWTs有到期值的原因。这些值都很短。通常的做法是将它保持在15分钟左右,这样任何泄漏的JWTs都会很快失效。但同时,也要确保捷豹突击队不会被泄露。

这两个事实几乎决定了处理越野车的所有特点!事实是捷豹突击队不应该被偷,他们需要有短的有效期,以防他们真的被偷了。

这就是为什么不将JWT存储在客户机上(比如通过cookie或本地存储)也非常重要的原因。这样做会使您的应用程序容易受到CSRF&;XSS的攻击,恶意表单或脚本会使用或窃取Cookie或本地存储中的令牌。

如果您解码Base64,您将获得3个重要部分JSON:头部、有效负载和签名。

JWT未加密。它是基于64编码和签名的。所以任何人都可以解码令牌并使用它的数据。JWT';的签名用于验证它实际上来自合法来源。

下面是如何发出JWT(/login),然后使用它来对另一个服务(/api)进行API调用的简图:

这是互联网上一场令人痛苦的讨论。我们简短(且固执己见)的答案是,后端开发人员喜欢使用JWT,因为a)微服务b)不需要集中式令牌数据库。

在微服务设置中,每个微服务可以独立地验证从客户端接收的令牌是否有效。微服务可以进一步解码令牌并提取相关信息,而无需访问集中式令牌数据库。

这就是API开发人员喜欢JWTs的原因,而我们(在客户端)需要弄清楚如何使用它。但是,如果您可以使用您最喜欢的单片框架颁发的会话令牌,那么您就完全可以使用了,并且可能不需要JWT!

现在我们已经基本了解了JWT是什么,让我们创建一个简单的登录流并提取JWT。这就是我们要达到的目标:

登录过程与您通常所做的并没有什么不同。例如,下面是一个登录表单,它向身份验证端点提交用户名/密码,并从响应中获取JWT令牌。这可以是使用外部提供商、OAuth或OAuth2步骤登录。这真的无关紧要,只要客户端最终在最后登录成功步骤的响应中获得一个JWT令牌即可。

首先,我们将构建一个简单的登录表单,将用户名和密码发送到登录服务器。服务器将发布JWT令牌,我们将其存储在内存中。在本教程中,我们不会关注auth服务器后端,但欢迎您在本文的示例repo中查看它。

登录API返回一个令牌,然后我们将该令牌从/utils/auth传递给登录函数,在那里我们可以决定在获得令牌后如何处理它。

我们需要将JWT令牌保存在某个地方,以便可以将其作为标头转发到API。您可能会忍不住将其保存在本地存储中;不要这样做!这很容易受到XSS攻击。

在客户机上创建cookie来保存JWT也很容易使用XSS。如果它可以在客户端上从您的应用程序之外的Javascript读取-那么它就可能被窃取。您可能认为HttpOnly cookie(由服务器而不是客户端创建)会有所帮助,但cookie容易受到CSRF攻击。值得注意的是,HttpOnly和合理的CORS策略不能阻止CSRF表单提交攻击,使用cookie需要适当的CSRF缓解策略。

请注意,新的SameSite cookie规范将使基于Cookie的方法不受CSRF攻击,该规范在大多数浏览器中得到了越来越多的支持。如果您的身份验证服务器和API服务器托管在不同的域上,这可能不是一个解决方案,但如果不是这样,它应该可以很好地工作!

现在,我们将其存储在内存中(我们将在下一节中介绍持久化会话)。

正如您在这里看到的,我们将令牌存储在内存中。是的,当用户在选项卡之间切换时,令牌将会失效,但我们将在稍后处理这一问题。我还将解释为什么我有noRedirect标志和JWT_TOKEN_EXPIRY。

在我们的API客户端中使用,将其作为标头传递给每个API调用。

通过查看是否设置了JWT变量来检查用户是否已登录。

或者,我们甚至可以解码客户端上的JWT以访问有效负载中的数据。假设我们需要客户端上的用户id或用户名,我们可以从JWT中提取它们。

我们检查utils/auth是否设置了标记变量,如果没有设置,则重定向到登录页面。

现在是设置我们的GraphQL客户端的时候了。我们的想法是从我们设置的变量中获取令牌,如果它在那里,我们将它传递给我们的GraphQL客户端。

假设您的GraphQL API接受JWT auth令牌作为Authorization标头,那么您需要做的就是设置客户端使用变量中的JWT令牌来设置HTTP标头。

正如您从代码中看到的,只要有令牌,它就会作为标头传递给每个请求。

这取决于应用程序中的流。假设您将用户重定向回登录页面:

假设我们的代币有效期只有15分钟。在这种情况下,我们可能会从拒绝我们的请求的API中得到一个错误(假设是401:未经授权的错误)。请记住,每个知道如何使用JWT的服务都可以独立地验证它,并检查它是否已经过期。

让我们将错误处理添加到我们的应用程序中来处理这种情况。我们将编写针对每个API响应运行的代码,并检查错误。当我们从API收到令牌过期/无效错误时,我们会触发注销或重定向到登录工作流。

您可能会注意到,这将导致相当糟糕的用户体验。每次令牌过期时,都会不断要求用户重新进行身份验证。这就是应用程序实现静默刷新工作流的原因,该工作流在后台不断刷新JWT令牌。下面几节将详细介绍这一点!

对于JWT,注销只是删除客户端上的令牌,这样它就不能用于后续的API调用。

实际上并不需要注销端点,因为任何接受您的JWT的微服务都会一直接受它。如果您的身份验证服务器删除了JWT,那也无关紧要,因为其他服务无论如何都会继续接受它(因为JWT的主要目的是不需要集中协调)。

这就是为什么将JWT过期值保持在一个较小的值很重要。这就是为什么确保你的捷豹突击队不会被偷更重要的原因。令牌是有效的(即使您在客户端将其删除),但仅在短时间内有效,以降低其被恶意使用的可能性。

此外,您还可以向您的JWT添加拒绝列出工作流。在这种情况下,您可以使用/logout API调用,并且身份验证服务器将令牌放入";无效列表";。但是,所有使用JWT的API服务现在都需要在其JWT验证中添加额外的步骤,以检查集中式拒绝列表。这再次引入了中央州,并将我们带回了使用JWTs之前的状态。

在某种程度上确实如此。这是一个可选的预防措施,如果您担心令牌可能会被窃取和误用,您可以采取此预防措施,但它也会增加必须完成的验证量。正如你可以想象的那样,这在互联网上引起了很多人的不满。

解决此问题的一种方法是在本地存储上引入全局事件侦听器。每当我们在一个选项卡上更新localstorage中的这个注销键时,侦听器都会在其他选项卡上触发并触发注销,并将用户重定向到登录屏幕。

在这种情况下,每当您从一个选项卡注销时,事件侦听器将在所有其他选项卡中触发,并将它们重定向到登录屏幕。

这可以跨选项卡使用。但是如何强制注销不同设备上的所有会话呢?!

我们将在后面的一节中更详细地介绍这个主题:强制注销。

我们基于JWT的应用程序的用户仍将面临两个主要问题:

鉴于我们在捷运专线上的到期时间很短,用户将每15分钟注销一次。这将是一次相当可怕的经历。理想情况下,我们可能希望我们的用户长时间登录。

如果用户关闭应用程序并再次打开,他们将需要重新登录。他们的会话不会持久,因为我们不会将JWT令牌保存在客户端的任何位置。

为了解决这个问题,大多数JWT提供程序都提供刷新令牌。刷新令牌有两个属性:

它可用于在前一个JWT到期之前进行API调用(比如/REFRESH_TOKEN)来获取新的JWT令牌。

此令牌作为身份验证过程的一部分与JWT一起颁发。身份验证服务器应该保存该刷新令牌,并将其与其自己的数据库中的特定用户相关联,以便它可以处理续订JWT逻辑。

在客户机上,在前一个JWT令牌到期之前,我们连接应用程序以创建/REFRESH_TOKEN端点并获取新的JWT。

刷新令牌由身份验证服务器作为HttpOnly cookie发送到客户端,并由浏览器在/REFRESH_TOKEN API调用中自动发送。

因为客户端Javascript不能读取或窃取HttpOnly Cookie,所以这在减轻XSS方面比将其作为普通Cookie持久化或保存在本地存储中稍好一些。

这不会受到CSRF攻击,因为即使表单提交攻击可以进行/REFRESH_TOKEN API调用,攻击者也无法获得返回的新JWT令牌值。总而言之,这就是我们如何思考持久化基于JWT的会话的最佳方式:?

在本地存储中持久化JWT令牌(易于使用XSS)<;在HttpOnly cookie中持久化JWT令牌(易于使用CSRF,对于XSS稍微好一点)<;在HttpOnly cookie中持久化刷新令牌(不受CSRF影响,对于XSS稍微好一点)。

请注意,虽然这种方法对严重的XSS攻击没有弹性,再加上通常的XSS缓解技术,但是推荐使用HttpOnly cookie来持久化与会话相关的信息。但是,通过通过刷新令牌间接地持久化我们的会话,我们可以防止使用JWT令牌时会出现的直接CSRF漏洞。

除了刷新令牌随JWT一起发送之外,没有什么太大的变化。让我们再看一看登录过程的示意图,但现在使用REFRESH_TOKEN功能:

服务器使用REFRESH_TOKEN设置HttpOnly Cookie。JWT_TOKEN和JWT_TOKEN_EXPIRY作为JSON有效负载返回给客户端。

服务器向客户端返回新的JWT_TOKEN和JWT_TOKEN_EXPIRY,并通过Set-Cookie报头设置新的刷新令牌cookie。

现在我们可以确保我们的用户不会一直被注销,让我们将注意力转向第二个持久化会话问题。

您会注意到,如果用户关闭您的应用程序并再次打开它(比方说关闭浏览器选项卡并重新打开它),他们将被要求再次登录。

应用程序通常会询问用户是否要在多个会话中保持登录状态,或者默认情况下是让用户保持登录状态。这也是我们想要实现的。

目前,我们不能这样做,因为JWT只存储在内存中,不是持久化的。总而言之,请转到上面的这一节,了解为什么我们不能将JWT直接存储在cookie或本地存储中。

刷新令牌!我们能够安全地持久化刷新令牌,并将其用于静默刷新(也就是续订即将到期的JWT令牌,而无需用户再次登录)。我们还可以使用它们为新会话获取新的JWT令牌!请查看上一节,讨论如何持久化刷新令牌。假设用户通过关闭浏览器选项卡注销了当前会话。现在用户再次访问该应用程序,让我们看看流程是什么样子:

如果我们发现内存中没有JWT,则会触发静默刷新工作流。

如果刷新令牌仍然有效(或尚未被吊销),那么我们将获得新的JWT,我们就可以开始工作了!

如果我们的刷新令牌过期(假设用户在很长一段时间后返回应用程序),或者被撤销(比方说因为强制注销),客户端将收到401错误的未经授权的REFRESH_TOKEN。另一种情况可能是我们一开始就没有任何REFRESH_TOKEN,在这种情况下,我们还会从/REFRESH_TOKEN端点收到一个错误,并且我们会将用户重定向到登录屏幕。

下面是一些示例代码,展示了我们将如何使用logoutLink处理此错误。

现在,用户已经永久登录,并且跨会话保持登录状态,因此我们需要担心一个新问题:强制注销或从所有会话和设备中注销。

以上各节中的刷新令牌实现向我们展示了我们可以持久化会话并保持登录状态。

在本例中,";force logout";的一个简单实现是要求身份验证服务器使与特定用户关联的所有刷新令牌无效。

这主要是在身份验证服务器后端实现的,不需要在客户端进行任何特殊处理。除应用程序上的强制注销按钮外,可能:)。

用户获得呈现的页面,然后继续使用应用程序作为SPA(单页应用程序)。

浏览器需要将有关当前用户身份的一些信息发送到SSR服务器。要做到这一点,唯一的方法是通过cookie。

因为我们已经通过cookie实现了刷新令牌工作流,所以当我们向SSR服务器发出请求时,我们需要确保刷新令牌也被发送。

注意:对于经过身份验证的页面上的SSR,auth API的域(因此REFRESH_TOKEN cookie的域)必须与SSR服务器的域相同,这一点至关重要。否则,我们的cookie将不会被发送到SSR服务器!

在接收到呈现特定页面的请求时,SSR服务器捕获REFRESH_TOKEN cookie。

SSR服务器使用REFRESH_TOKEN cookie为用户获取新的JWT。

SSR服务器使用新的JWT令牌,并发出所有经过身份验证的GraphQL请求以获取正确的数据。

不,不幸的是,如果没有一些额外的摆弄,就不会!一旦SSR服务器返回呈现的HTML,浏览器上关于用户身份的唯一标识就是已经由SSR服务器使用的旧刷新令牌cookie!

如果我们的应用程序代码尝试使用这个刷新令牌cookie来获取新的JWT,则此请求将失败,并且用户将被注销。

要解决这个问题,SSR服务器在呈现页面后需要发送最新的刷新令牌cookie,以便浏览器可以使用它!

这里提供了这篇带有端到端工作应用程序、具有SSR功能的博客帖子的示例代码。

一旦您完成了上面的所有部分,您的应用程序现在应该具有使用JWT的现代应用程序的所有功能,并且应该是安全的,不会受到JWT实现所具有的常见主要安全问题的影响!

如果您有任何问题、建议或反馈,请在Twitter或下面的评论中告诉我们!

Hasura是一个开源引擎,在新的或现有的Postgres数据库上为您提供实时的GraphQL API,内置支持缝合自定义GraphQL API并在数据库更改时触发WebHook。