在 Golang 应用程序中使用 JWT 进行身份验证

Published September 07, 2020 by Victor Steven
Categories:

博客聚焦横幅

介绍

JSON 网络 token (JWT) 是一种紧凑且独立的方法,以 JSON 对象的形式在各方之间安全地传输信息,开发人员通常将之用于其 API 中。JWT 之所以受欢迎,是因为:

  1. JWT 是无状态的。也就是说,与不透明 token 不同,它不需要存储在数据库(持久层)中。
  2. JWT 的签名一旦形成就永远不会解码,从而确保 token 的安全性。
  3. 可以将 JWT 设置为在特定时间段后无效。这有助于在 token 被劫持的情况下,最大限度地减少或完全消除黑客可能造成的任何损害。

在本教程中,我将使用 Golang 和 Vonage Messages API 通过简单的 RESTful API 演示 JWT 的创建、使用和失效。

Vonage API 帐户

要完成本教程,您将需要一个 Vonage API 帐户。如果您还没有帐户,则可以立即注册,并使用免费积分开始构建。拥有帐户后,您可以在 Vonage API Dashboard 的顶部找到您的 API 密钥和 API 密码。本教程还使用到虚拟电话号码。如要购买,请转到“号码” > “购买号码”并搜索满足您需求的号码。如果您刚刚注册,可使用可用积分轻松抵扣号码的初始费用。

开始使用 Vonage 构建

JWT 由什么组成?

JWT 由三部分组成:

  • 标头:token 的类型和使用的签名算法。
    token 的类型可以是“JWT”,而签名算法可以是 HMAC 或 SHA256。
  • 有效负载:token 中包含声明的第二部分。这些声明包括特定于应用程序的数据(例如:用户 ID、用户名)、token 到期时间 (exp)、颁发者 (iss)、主题 (sub) 等。
  • 签名:编码的标头、编码的有效负载和您提供的密码用于创建签名。

让我们使用一个简单的 token 来理解以上概念。

token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoX3V1aWQiOiIxZGQ5MDEwYy00MzI4LTRmZjMtYjllNi05NDRkODQ4ZTkzNzUiLCJhdXRob3JpemVkIjp0cnVlLCJ1c2VyX2lkIjo3fQ.Qy8l-9GUFsXQm4jqgswAYTAX9F4cngrl28WJVYNDwtM

别担心,此 token 无效,不会对任何生产应用程序生效。

您可以导航到 jwt.to 并测试 token 签名是否已验证。使用“HS512”作为算法。您将收到消息“签名已验证”:

要进行签名,您的应用程序需要提供密钥。此密钥使签名能够保持安全性——即使在对 JWT 进行解码时,签名仍保持加密状态。强烈建议在创建 JWT 时始终使用密码。

token 类型

由于 JWT 可以设置为在特定时间段后到期(失效),因此在此应用程序中将考虑两个 token :

  • 访问 token:访问 token 用于需要身份验证的请求。通常将其添加到请求的标头中。建议将访问 token 的使用寿命设置为较短寿命,例如 15 分钟。如果在 token 被劫持的情况下篡改了用户的 token ,则在较短的时间范围内授予访问 token 可以防止任何严重的损害。在 token 失效之前,黑客只有 15 分钟或更短的时间执行操作。
  • 刷新 token:刷新 token 的使用寿命较长,通常为 7 天。该 token 用于生成新的访问和刷新 token。如果访问 token 到期,则在(通过我们的应用程序)命中刷新 token 路由时,会创建新的访问 token 集和刷新 token 集。

JWT 的存储位置

对于生产级应用程序,强烈建议将 JWT 存储在 {0} Cookie 中。为此,在将从后端生成的 Cookie 发送到前端(客户端)时,会随 Cookie 发送一个 HttpOnly 标志,指示浏览器不要通过客户端脚本显示 Cookie。这样做可以防止 XSS(跨站点脚本)攻击。
JWT 也可以存储在浏览器本地存储或会话存储中。通过这种方式存储 JWT 会使其受到多种攻击,例如上述 XSS,因此与使用 HttpOnly Cookie 技术相比,它的安全性通常较低。

应用程序

我们将考虑一个简单的待办事项RESTful API。

创建一个名为“{1}”的目录,然后初始化 {2} 进行依赖关系管理。{3} 正在初始化,使用:

现在,在根目录 ({5}) 中创建一个 {4} 文件,并向其添加以下内容:

我们将使用 gin 来选择路由和处理 HTTP 请求。Gin 框架有助于减少样板代码,并且在构建可扩展 API 方面非常高效。

您可以使用以下方法安装 gin(如果尚未安装):

然后更新 {6} 文件:

在理想情况下, /login 路由会获取用户的凭据,将其与某些数据库进行比较,然后在凭据有效时进行登录。但是在此 API 中,我们将仅使用将在内存中定义的示例用户。在结构中创建一个示例用户。将此添加到 {7} 文件:

登录请求

验证用户的详细信息后,将会登录用户并代表他们生成 JWT。我们将在下面定义的 {8} 函数中实现此目的:

我们收到了用户的请求,然后将其打乱为 {9} 结构。然后,我们将输入用户与我们在内存中定义的用户进行了比较。如果我们使用的是数据库,则将其与数据库中的记录进行比较。

为了不使 {10} 函数膨胀,生成 JWT 的逻辑由 {11} 处理。注意,用户 ID 传递给了此函数。生成 JWT 时用作声明

{12} 函数利用了 {13} 包,我们可以使用以下命令进行安装:

我们来定义 {14} 函数:

我们将 token 设置为仅在 15 分钟内有效,在此之后,token 无效并且不能用于任何经过身份验证的请求。另请注意,我们使用从环境变量中获得的密码 ({15}) 签署 JWT。强烈建议您不要在代码库中公开此密码,而是如上所示从环境中调用此密码。您可以将其保存在 {16}、{17} 或任何适合您的位置。

到目前为止,我们的 {18} 文件如下所示:

现在,我们可以运行该应用程序:

现在我们可以尝试一下,看看效果如何!启动您喜欢的 API 工具并点击 https://www.nexmo.com/wp-content/uploads/2020/03/image8.png端点:

如上所示,我们生成了一个可持续 15 分钟的 JWT。

实施漏洞

是的,我们可以登录用户并生成 JWT,但是上述实施存在很多错误:

  1. JWT 只能在到期时失效。这方面的一个主要限制是:用户可以登录,然后决定立即注销,但用户的 JWT 仍然有效,直至达到到期时间为止。
  2. JWT 可能会被黑客劫持和使用,而用户却没有采取任何对策,直至 token 到期为止。
  3. token 到期后,用户将需要重新登录,从而导致用户体验不佳。

我们可以通过两种方式解决上述问题:

  1. 使用持久性存储层存储 JWT 元数据。这将使我们能够在用户退出的一瞬间使 JWT 失效,从而提高安全性。
  2. 利用刷新 token的概念,在访问 token过期的情况下,生成一个新的访问 token,从而提高用户体验。

使用 Redis 存储 JWT 元数据

上面我们提出的一个解决方案是将 JWT 元数据保存在持久层中。可以在选择的任何持久层中完成此操作,但强烈建议使用 redis。由于我们生成的 JWT 具有到期时间,因此 redis 具有自动删除已达到到期时间的数据的功能。Redis 还可以处理大量写入操作,并且可以水平扩展。

由于 redis 是键值存储,因此其键必须是唯一的,要实现这一点,我们会将 {20} 用作键,并将用户 ID 用作值。

因此,我们来安装两个要使用的软件包:

我们还会将它们导入 {21} 文件中,如下所示:

注意:希望此前您已在本地计算机上安装了 redis。否则,您可以先暂停并进行安装,然后再继续。

我们现在来初始化 redis:

Redis 客户端在 {22} 函数中初始化。这样可以确保每次我们运行 {23} 文件时,redis 都会自动连接。

从这一点开始创建 token 时,我们将生成一个 {24} ,它将用作 token 声明之一,就像在前面实施中将用户 ID 用作声明一样。

定义元数据=

在我们提出的解决方案中,我们需要创建两个 JWT,而不是只创建一个 token:

  1. 访问 token
  2. 刷新 token

要实现这一点,我们需要定义一个结构来包含这些 token 定义及其有效期限和 uuid:

有效期限和 uuid 非常方便,因为在 redis 中保存 token 元数据时会用到它们。

现在,让我们将 {25} 函数更新为如下所示:

在以上函数中,访问 token在 15 分钟后到期,刷新 token在 7 天后到期。您还会注意到,我们为每个 token 添加了一个 uuid 作为声明。
由于 uuid 在每次创建时都是唯一的,因此用户可以创建多个 token。当用户在其他设备上登录时,就会发生这种情况。用户还可以从任何设备注销,而无需从所有设备注销。真棒!

保存 JWT 的元数据

现在我们来连接将用于保存 JWT 元数据的函数:

我们传入 https://www.redily.app ,其中包含有关 JWT 的到期时间和创建 JWT 时使用的 uuid 的信息。如果刷新token访问token都达到了到期时间,则会从 redis 中自动删除 JWT。

我个人使用 Redily (redis GUI)。这是一款很好的工具。您可以在下面查看如何在键值对中存储 JWT 元数据。

在再次测试登录之前,我们需要在 {7} 函数中调用 {7} 函数。更新登录函数:

我们可以尝试再次登录。保存 https://www.nexmo.com/wp-content/uploads/2020/03/image3.png 文件并将其运行。当邮递员点击登录时,我们应该具有:

太棒了!我们既有 access_tokenrefresh_token,也有 token 元数据持久保存在 redis 中。

创建待办事项

现在,我们可以继续使用 JWT 进行身份验证的请求。

此 API 中未经验证的请求之一是创建待办事项请求。

首先,我们来定义一个 {30} 结构:

执行任何经过身份验证的请求时,我们需要验证在身份验证标头中传递的 token,以查看其是否有效。我们需要定义一些辅助函数来协助这些操作。

首先,我们需要使用 {31} 函数从请求标头中提取 token:

然后,我们将验证 token:

我们在 {33} 函数内调用了 {32} 以获取 token 字符串,然后继续检查签名方法。

然后,我们将使用 {34} 函数检查此 token 的有效性,了解其仍然有用或是已过期:

我们还将提取 token元数据,这些元数据将在我们之前设置的 redis 存储中进行查找。要提取 token,我们定义了 ExtractTokenMetadata 函数:

{35} 函数返回一个 {36} (这是一个结构)。此结构包含了我们在 redis 中进行查找所需要的元数据({37} 和 {38})。如果出于任何原因我们无法从此 token 中获取元数据,则该请求将暂停并显示一条错误消息。

上面提到的 {39} 结构如下所示:

我们还提到了在 redis 中查找 token 元数据。我们来定义一个能够实现此操作的函数:

FetchAuth() 从 {42} 函数接受 {41},然后在 redis 中查找。如果找不到记录,则可能意味着 token 已过期,因此引发错误。

最后,我们来连接 {43} 函数,以便更好地理解上述函数的实施:

如上所示,我们调用 {44} 来提取 {45} 中使用的 JWT 元数据,以检查该元数据是否仍然存在于我们的 redis 存储中。如果一切正常,则可以将待办事项保存到数据库中,但是我们选择将其返回给调用方。

我们来更新 {46} 以包含 {47} 函数:

要测试 https://www.nexmo.com/wp-content/uploads/2020/03/image6.png,请登录并复制 https://www.nexmo.com/wp-content/uploads/2020/03/image6.png,然后将其添加到Authorization Bearer Token 字段,如下所示:

然后在请求正文中添加标题以创建待办事项并向 https://www.nexmo.com/wp-content/uploads/2020/03/image4.png 端点发出 POST 请求,如下所示:

在没有 https://www.nexmo.com/wp-content/uploads/2020/03/image5.png 的情况下尝试创建待办事项是未经授权的行为:

注销请求

到目前为止,我们已经了解如何使用 JWT 来进行认证请求。当用户注销时,我们将立即撤消其 JWT 并使之失效。这是通过从 redis 存储中删除 JWT 元数据来实现的。

现在,我们将定义一个函数,使我们能够从 redis 中删除 JWT 元数据:

上面的函数将删除 redis 中与作为参数传递的 {52} 对应的记录。

{53} 函数如下所示:

在 {54} 函数中,我们首先提取 JWT 元数据。如果成功,我们将继续删除该元数据,从而立即使 JWT 无效。

在测试之前,更新 {55} 文件以包含 {56} 端点,如下所示:

提供与用户关联的有效 https://www.nexmo.com/wp-content/uploads/2020/03/image1.png,然后注销该用户。记得将 https://www.nexmo.com/wp-content/uploads/2020/03/image1.png 添加到 https://www.nexmo.com/wp-content/uploads/2020/03/image1.png,然后单击注销端点:

现在用户已注销,由于该 JWT 立即失效,因此无法再次对该 JWT 执行进一步的请求。这种实施方式比在用户注销后等待 JWT 到期更为安全。

保护经过验证的路由

我们有两个需要身份验证的路由:{60} 和 {61}。现在,无论是否通过身份验证,任何人都可以访问这些路由。我们来改变这种状况。

我们将需要定义 {62} 函数来保护这些路由:

如上所示,我们调用了 {63} 函数(前面已定义)来检查 token 是否仍然有效或已过期。该函数将用于经过身份验证的路由以保护它们。
现在我们来更新 {64} 以包含此中间件:

刷新 token

到目前为止,我们可以创建、使用和撤消 JWT。在会涉及用户界面的应用程序中,如果访问 token到期且用户需要发出经过身份验证的请求,会发生什么情况?用户是否会被设为未经授权并且需要再次登录?很遗憾,情况就是这样。但这可以使用刷新 token的概念来避免。用户不需要重新登录。
访问 token一起创建的刷新 token将用于创建新的访问 token 和刷新 token对。

利用 JavaScript 使用 API 端点,我们可以使用 axios 拦截器轻松地刷新 JWT。在我们的 API 中,我们需要将带有 refresh_token 作为主体的 POST 请求发送到 /token/refresh 端点。

首先我们来创建 {1} 函数:

虽然该函数中有大量工作,但我们来尝试了解一下流程。
– 我们首先从请求正文中获取了 {66}。
– 然后,我们验证了 token 的签名方法。
– 接下来,检查 token 是否仍然有效。
– 然后提取 {67} 和 {68},它们是创建刷新 token 时用作声明的元数据。
– 然后,我们在 redis 存储中搜索元数据,并使用 {69} 作为键将其删除。
– 然后,我们创建一对新的访问和刷新 token,这些 token 现在将用于将来的请求。
– 访问 token 和刷新 token 的元数据保存在 redis 中。
– 创建的 token 返回给调用者。
在 else 语句中,如果刷新 token无效,则不允许用户创建新的 token 对。我们将需要重新登录以获得新token。

接下来,在 {70} 函数中添加刷新 token 路由:

使用有效的 https://www.nexmo.com/wp-content/uploads/2020/03/image7.png 测试端点:

我们已成功创建了新的 token 对。太好了😎。

使用 Vonage Messages API 发送消息

让我们在用户每次使用 Vonage Messages API 创建待办事项时通知他们。

您可以在环境变量中定义 API 密钥和密码,然后在此文件中使用它们,如下所示:

然后,我们将定义一些具有发送者、接收者和消息内容信息的结构。

然后,我们在下面定义向用户发送消息的功能:

在以上函数中,https://dashboard.nexmo.com 号码为用户号码,而 https://dashboard.nexmo.com 号码必须通过您的 Vonage API Dashboard 进行购买。

确保在环境变量文件中定义了 {1} 和 {1}。

然后,我们更新 {1} 函数以包含刚刚定义的 {1} 函数,并传入所需的参数:

确保提供了有效的电话号码,以便在尝试创建待办事项时能够收到消息。

结语

您已经了解了如何创建 JWT 并使 JWT 失效。您还了解了如何在 Golang 应用程序中集成 Vonage Messages API 来发送通知。有关最佳实践和使用 JWT 的更多信息,请务必查看此 GitHub 存储库 。您可以扩展此应用程序,并使用真实的数据库来保留用户和待办事项,还可以使用 React 或 VueJS 来构建前端。在那里,您将真正受益于 Axios 拦截器的刷新 token 功能。

Originally published at https://www.nexmo.com/blog/2020/03/13/using-jwt-for-authentication-in-a-golang-application-dr

Leave a Reply

Your email address will not be published.

Get the latest posts from Nexmo’s next-generation communications blog delivered to your inbox.

By signing up to our communications blog, you accept our privacy policy , which sets out how we use your data and the rights you have in respect of your data. You can opt out of receiving our updates by clicking the unsubscribe link in the email or by emailing us at [email protected].