写点什么

JWT 真的安全吗?如何解决该问题

  • 2023-05-31
    湖南
  • 本文字数:12899 字

    阅读完需:约 42 分钟

JWT 并不是安全的

本文是基于 JSON Web Tokens(JWTs) Are Not Safe 这一本书所描述的问题,基于自己的理解,做出的总结,文章中的内容,也是大多出于该书,仅供学习使用。

一、概述

Json Web Tokens(jwt)在用户的会话管理上很流行。但是呢有很多的文章和专家提出jwt存在潜在的危险和效率比较低的问题。然而,这些警告被一些人员如营销人员、知识博主、课程创建者等有意无意的进行掩盖了(作者说的,不是我说的)。


使用jwt的主要原因之一是对速度的需求。另一个原因是它使用起来很简单。最后一个原因是,它是一个值得关注和友好的名字,对市场营销很有帮助。这个名字结合了JSON(通常很受欢迎)、Web(用于 Web)和Token(意味着无状态),所有这些都可能使人们认为它非常适合他们的 Web 身份验证。在现实中,并非如此:在本篇文章的最后,您将了解jwt的好处和危险,以及成千上万的公司用来克服这个问题的经过实战考验的解决方案。


整本书大概分为了 6 个主题:

  1. Chapter1:HTTP 会话、认证和授权

  2. Chapter2:在传统数据库中存储会话。

  3. Chapter3:在 JWT 中存储会话

  4. Chapter4:在 Redis 中存储会话

  5. Chapter5:当 Redis 是你的主数据库时

  6. Chapter6:同时使用 Redis 和 JWT


这 6 个主题将不断的深入探讨这个话题,最后提出解决方案。

二、Chapter1:HTTP 会话、认证和授权

假设您正在使用微博。您登录该平台,点赞某人的微博,然后发布了自己的新微博。在登录后,您需要进行身份验证和授权,才能执行这三个特定操作中的每一个。这是因为 HTTP 是一种无状态协议(stateless),这意味着 HTTP 请求在一个请求到下一个请求之间不会存储您的身份信息。

在您登录后,服务器通常会创建一个会话。会话是一个容器,包含有关用户与网站或服务交互的数据,并且根据其名称,其生命周期通常受限于用户的登录和注销操作。一个会话通常会包含以下信息:

  • 用户的个人资料信息,如姓名、出生日期、电子邮件地址等;

  • 用户的权限,例如“用户”、“管理员”、“监督员”、“超级管理员”等;

  • 其他与应用程序相关的数据,例如购物车细节(如果是零售应用程序)等;

  • 会话过期时间,例如从现在开始一小时、从现在开始一周等等。


管理会话面临五个主要挑战:

  1. 会话数据需要存储在某处;

  2. 由于 HTTP 是无状态的,因此必须将会话数据发送回客户端,以便客户端可以将此信息添加到未来的请求中;

  3. 然后客户端需要将会话数据发送回服务器,以供未来的请求使用;

  4. 服务器需要验证客户端的信息是否有效,即“身份验证”和“授权”。例如,是否点赞推文的用户是“用户”或“管理员”,会话是否过期等;

  5. 会话过期,某个时刻,为了安全起见,会话需要过期,迫使人们重新登录。


让我们来看看这五点,了解它们在大多数应用程序中的工作原理。

2.1 在哪存会话数据

如果将会话数据发送回客户端(如浏览器或移动应用程序),则存在安全问题,因为可能会有人访问或拦截会话数据并更改数据来访问服务器。此外,在客户端和服务器之间可能有大量的数据来回传输,因此需要将其存储在后端,通常是数据库中。

2.2 如何向客户端发送会话数据

如果在服务器上创建了会话(登录后),并将该数据保留在数据库中,那么客户端如何知道它呢?为了解决这个问题,服务器会生成一个会话令牌,它看起来像一个指向数据库中实际会话数据的随机字符串,并将其发送回给客户端。服务器可以将会话令牌作为 cookie 或 HTTP 响应的形式发送。会话令牌是一个不透明的随机字符串,看起来像这样:fsaf12312dfsdf364351312srw12312312dasd1et3423r


在数据库中,该字符串指向整个会话数据,类似于下面这个样子:

2.3 客户端如何向服务器发送会话令牌以用于将来的请求

一旦客户端以 cookie 或令牌的形式接收到会话令牌,它就会保留此信息并将此会话令牌信息添加到以后的每个请求中:

下面是它的工作原理:

  1. 用户登录

  2. 服务器创建一个会话、会话令牌和 cookie

  3. 服务器将会话和会话令牌存储到数据库中

  4. 然后,服务器将包含会话令牌的 cookie 发送回浏览器

2.4 服务器如何处理身份验证和授权

在每个后续的请求中,服务器使用会话令牌查询数据库,获取实际的会话,然后检查两个方面:

  1. 身份验证:您的登录数据仍然有效(验证是否被篡改、是否过期、是否已注销等等)。

  2. 授权:您可以登录,但是您是否具有执行该特定操作的权限?(即检查您是否是管理员、数据所有者、用户、雇员、超级管理员等等)

2.5 会话何时到期

每个会话还有一个过期时间,后端开发人员可以将其设置为 5 分钟到 30 天之间的任何时间。在设置的时间之后,会话数据将被删除。如果用户请求执行某些操作,通常会拒绝其权限,大多数客户端应用程序都会重定向用户到登录页,迫使其重新登录。当他们重新登录时,会创建一个新的会话,具有新的过期时间并开始新的循环。


注意:如果使用 OAuth 进行身份验证,将获得多个令牌,如“访问令牌”、“刷新令牌”等。这些令牌都可用于更细粒度的控制会话何时应该过期。例如,客户端可以使用刷新令牌来延长会话的时间,而不是将人们注销。

2.6 本节总结

  • HTTP 协议是无状态的,所以为了在登录后跟踪用户,创建了一个“会话”。

  • 会话是关于用户及其活动的数据。它包含了用户是谁,以及他们被授权和认证去做什么,还可用于跟踪任何特定的产品相关数据。

  • 会话数据通常存储在数据库中。

  • 创建指向会话的“会话令牌”,并将其发送给客户端以供以后参考。

  • 客户端发送每个未来请求的此“会话令牌”(通过请求头或通过 cookie),以识别用户和存储在会话中的其他详细信息。

  • 服务器通过进行额外的数据库查询从会话令牌中检索会话,检查是否存在有效会话,如果会话令牌和会话本身都有效,则允许用户执行未来操作,例如喜欢一条推文,创建一条推文等等。

三、在传统的数据库中存储会话

您仅仅的了解了session工作的原理,现在,让我们继续以微博为例,看看当您登录了微博和发布了一条博文的整个过程。


您使用用户名和密码登录:

  1. 服务器首先对用户进行身份验证。

  2. 然后,服务器创建一个会话令牌,并将该令牌与用户信息一起存储在某个数据库中。

  3. 然后,服务器向前端移动设备或 Web 应用程序发送会话令牌。

  4. 与您的博文一起,应用程序还会发送会话令牌(通过 cookie 或标题),以便服务器可以识别您是谁(但请记住,令牌只是一个随机字符串,那么服务器如何才能从会话令牌中知道您是谁呢?)

  5. 当服务器接收到会话令牌时,它不知道是哪个用户,因此将其发送到数据库中,从该令牌中检索实际用户的信息(如 UserID)。

  6. 如果用户存在并被允许完成该操作(即发送博文),则服务器允许他们执行操作。

  7. 最后,它告诉前端博文已发送。

3.1 这个过程的主要问题

这种方法的主要问题在于第四步骤很慢,并且需要针对用户执行的每个操作重复执行。因此,每个 API 调用至少会导致两个缓慢的数据库调用,这可能会降低整体响应时间。解决这个问题有两种方法:

  1. 完全消除对用户进行的数据库查找(即消除第四步骤)。

  2. 使额外的数据库查询更快,以便其他跳跃不会影响性能。

3.2 解决方法-消除数据库的查找

  1. 将状态存储在服务器的内存中。但是,此状态只在特定服务器上可用,这在规模上造成问题。

  2. 使用“粘性会话”。这是指指示负载均衡器始终将流量定向到特定的服务器,即使在扩展后也一样。同样,这可能会导致不同的扩展问题,并且如果服务器崩溃(缩小范围),则会丢失所有会话。

  3. 使用 JSON Web 令牌。我们将在下一章中研究如何实现这一点。

3.3 解决方法-使用告诉缓存

使 Redis 数据库查询,速度如此快,以至于额外的调用不会影响性能简单地使用 Redis。成千上万的公司使用 Redis 进行会话存储。拥有次微秒的延迟,就像将此数据存储在服务器本身中一样。我们稍后会更深入地了解这个问题。

四、在会话中存储 JWT

JWT,特别是用作会话,尝试通过完全消除数据库查找来解决耗时的重复数据库调用问题。


其主要思想是将用户信息存储在会话令牌本身中,这意味着用户信息被传递到会话令牌而不是某些长的随机字符串中。为了确保其安全性,令牌的一部分使用服务器唯一知道的密钥进行了签名。因此,即使客户端和服务器可以看到令牌的用户信息部分,第二部分(已签名的部分)只能由服务器进行验证。在下面的示例中,令牌的粉色部分包含有效载荷(用户信息),可以由客户端和服务器看到。

但是,蓝色部分使用头文件和有效载荷本身进行签名。因此,如果客户端篡改有效载荷(比如冒充另一个用户),签名将不同,无法进行认证。

上述图片显示了一个 JWT 令牌。它包括header(红色突出显示)和payload(紫色突出显示)通常不加密(只进行 base64 编码),但signature(蓝色突出显示)是签名的。


下面是使用 JWT 的示例流程:

  1. 输入用户名和密码进行登录: 1->服务器通过查询数据库对用户进行身份验证 2->服务器使用用户信息和密码创建 JWT 会话令牌(没有涉及数据库)

  2. 服务器将 JWT 令牌发送给前端应用程序,以便用户可以发送 JWT 令牌来标识用户,而不是每次都要进行登录。

  3. 接下来,假设您再次编写并提交博文。当您发送博文时,您的应用程序将与您的博文文本一起发送 JWT 令牌(通过 cookie 或标头),以便服务器可以确定您的身份。但是,服务器如何仅从 JWT 令牌就可以知道您的身份呢?令牌的部分已经包含了用户信息。

  4. 因此,当服务器接收到 JWT 令牌时,它使用密钥字符串来验证已签名部分并从有效载荷部分获取用户信息,从而消除 DB 调用

  5. 如果验证签名,允许他们执行该操作。

  6. 最后,向前端发送推文已保存的消息(即,用户最初要执行的操作的结果)。


对于每个用户操作,服务器只需验证已签名的部分,获取用户信息,并让用户完成该操作,从而有效地完全跳过 DB 调用。


这样做有什么问题呢?

JWT 令牌通常设置了 5 到 30 分钟的过期时间,并且不能轻易地使其失效或更新。这是 JWT 令牌的一个显著限制。


JWT 规范的编写方式非常“宽松”,类似于 HTML 规范,这可能会通过允许适用于边缘情况和各种用例的变通方法导致安全漏洞。这种方法可能会使后端工程师和库创建者难以避免漏洞和最佳实践,从而导致安全漏洞。与 HTML 导致不良网页渲染不同,如果未遵循 JWT 的最佳实践,可能会导致认证不安全等更严重的后果。


JWT 库创建者通常遵守宽松的 JWT 规范,因此后端工程师在使用 JWT 令牌时必须小心,因为根据规范,在技术上可能是有效的,但可能不安全。

4.1 问题 1 The none algorithm

JWT 允许使用各种算法对有效负载进行签名,其中之一是“none”算法。在高层次上,如果有人指定算法“none”,这意味着 JWT 库应完全忽略对签名的验证。因此,攻击者只需要将算法类型更改为“none”,然后向服务器发送任何他们想要的内容。库将认为无需验证签名并允许无条件访问。


例如,假设攻击者在头文件中传递“alg”=none,并且您正在从下面的请求头中读取 alg(req header alg)——这是有效的——那么您可能会遭遇此漏洞。


解决此问题的方法是后端工程师需要确保忽略“none”算法。即使是 Auth0 也发生了严重的安全问。

 // client {     alg:'none' }
复制代码


 // server const token = jwt.sign(payload,secret,{algorithm:req.header.alg})
复制代码

这个问题的解决方案是后端工程师需要确保他们忽略none算法甚至是管理员。

4.2 问题 2 The algorithms are passed in an array

JWT 允许在验证期间在一个数组中指定算法。这会打开另一种类型的攻击。虽然很复杂,但要点是,当您在数组中传递算法时,库开始检查其中一种算法是否适用于有效负载。显然,您可以利用它,因为您可以使用 RS256 密钥作为 HS256 密钥。因此,如果您有以下代码,攻击者可以将 RS256 令牌用作 HS256 密钥并绕过 JWT 安全测试。


当 JWT 验证过程中允许在一个数组中指定算法时,可能会导致一种攻击。下面是一个示例来说明这种攻击的情况:


假设有以下代码片段用于验证 JWT 令牌:

 algorithm = token['alg'] payload = token['payload'] ​ if algorithm == 'HS256':     # 使用HS256密钥验证签名     verify_signature_hmac(payload, secret_key) elif algorithm == 'RS256':     # 使用RS256公钥验证签名     verify_signature_rsa(payload, public_key) else:     # 不支持的算法     raise Exception('Unsupported algorithm')
复制代码

在这个例子中,令牌的alg字段指定了所使用的算法,可以是HS256(HMAC-SHA256)或RS256(RSA-SHA256)。如果攻击者能够修改令牌,他们可以将alg字段设置为RS256,并将有效负载中的公钥替换为 HS256 密钥。


具体步骤如下:

  1. 攻击者创建一个 JWT 令牌,将alg字段设置为RS256,伪造有效负载中的数据,如下所示:

 jsonCopy code{   "alg": "RS256",   "payload": {     "username": "admin",     "role": "admin"   } }
复制代码
  1. 攻击者使用 RS256 算法来签名令牌,但实际上使用的是 HS256 密钥。这意味着签名是使用 RS256 算法,但使用 HS256 密钥生成的。

  2. 攻击者将伪造的 JWT 令牌发送给目标服务器进行验证。

  3. 目标服务器的验证代码检查alg字段,发现是RS256,然后使用 RS256 公钥来验证签名。但实际上,签名是使用 HS256 密钥生成的,因此验证失败。

  4. 由于验证失败,目标服务器可能拒绝该令牌或不正确地授予权限。


通过这种方式,攻击者成功绕过了 JWT 的安全性,使用一个错误的算法类型和伪造的密钥来进行欺骗。这就是为什么在实现 JWT 时需要小心处理算法选择和验证逻辑,以确保令牌的完整性和安全性。

4.3 问题 3 Claims are optional

声明(Claims)是可选的。


JWT 提供了一种很好的方式来组织和确保各种声明,从而提高安全性,但它们都是可选的。例如,在右侧的示例代码中,sub、iss、aud 等都是可选的。这就让实现者必须遵循最佳实践。如果规范将其中一些声明设为必填项,将会解决很多安全方面的问题。


举个例子,如果"aud"是强制性的,它将迫使工程师们思考并确保一个服务(例如"CartService")的 JWT 不能适用于另一个服务(例如"BackOfficeService")。由于这些声明是可选的,它们通常不是必需的,或者配置不正确。为了解决这个问题,必须设置声明并检查其是否符合预期的要求。

4.4 其他问题

  1. 令牌长度:在许多复杂的实际应用中,您可能需要存储大量信息,并且将其存储在 JWT 令牌中可能会超过允许的 URL 长度或 Cookie 长度,从而导致问题。此外,现在您可能需要在每个请求中发送大量数据。

  2. 需要维护状态:无论如何(用于速率限制、IP 白名单等),服务器都需要维护用户的状态。在许多实际应用中,服务器必须维护用户的 IP 并跟踪 API 以进行速率限制和 IP 白名单。因此,无论如何,您仍然需要使用一个高效的数据库。认为 JWT 可以使应用程序变得无状态是不现实的。

4.5 那么为什么 JWT 对用户身份验证来说是危险的呢?

除了上述所有问题外,JWT 的最大问题是令牌撤销问题。由于令牌在过期之前都可以继续使用,服务器没有简单的方法来撤销令牌。


以下是一些可能导致这种情况危险的使用情况:

  1. 注销并不真正使您注销。想象一下,在发送推文后,您从微博注销了。您可能认为您已从服务器注销,但事实并非如此,因为 JWT 是自包含的,在它过期之前将继续工作。这个过期时间可能是 5 分钟、30 分钟或其他作为令牌的一部分设置的时间。因此,如果有人在此期间获得该令牌,他们可以继续使用它进行身份验证,直到它过期。

  2. 阻止用户并不会立即阻止他们。想象一下,您是微博的一名版主或某个在线实时游戏的版主,真实用户在使用系统。作为版主,您希望快速阻止某人滥用系统。出于同样的原因,您无法立即实现这一点。即使在您阻止他们之后,用户仍将继续访问服务器,直到令牌过期。

  3. JWT 可能包含过期的数据。想象一下,用户是管理员,但被降级为具有更少权限的普通用户。同样,这不会立即生效,用户将继续保持管理员权限,直到令牌过期。

  4. JWT 通常不会加密。因此,任何能够执行中间人攻击并嗅探 JWT 的人现在都拥有您的身份验证凭据。这变得更容易,因为中间人攻击只需要在服务器和客户端之间的连接上完成。


有一种加密 JWT 令牌的方法称为 JWE,但是当使用此方法时,客户端(特别是浏览器和移动设备)无法解密令牌以查看实际有效载荷。此时,从 Web 应用程序和移动应用程序的角度来看,您实际上是在使用 JWT 作为常规的加密会话令牌。

4.6 还要使用吗?

尽管您可能已经解决了所有规范问题并且只想解决过期问题,但您仍然在努力使 JWT 工作?


假设您已经解决了所有规范问题,并且只想解决过期问题。一个常见的解决方案是在数据库中存储一个“已撤销令牌”列表,并在每个调用中检查该列表。如果令牌属于被撤销列表的一部分,则阻止用户进行下一步操作。但是现在您需要额外的数据库调用来检查令牌是否被撤销,这就完全抵消了使用 JWT 的整个目的。

下面的图表很好地说明了使用 JWT 的挑战以及所有解决方法的问题。


您可以尝试五种不同的方法来改进 JWT,但在这五种情况下,都会遇到一种或另一种瓶颈。您应该问自己,为什么需要使用一种需要这么多解决方案的技术?而且,当您实施了所有解决方案以满足自己的要求时,您可能会失去最初使用它的好处。


图表说明了使用 JWT 的挑战和所有解决方法的问题。

    Challenges of Using JWT   +-------------------------------------------------------+   |                   Token Expiration                     |   +-------------------------------------------------------+   |             - Revoked Token Database                   |   |             - Additional DB calls                      |   +-------------------------------------------------------+   |             Token Revocation Problem                   |   +-------------------------------------------------------+   |             - Inability to easily revoke tokens         |   |             - Lack of real-time revocation              |   +-------------------------------------------------------+   |             Length of Tokens                            |   +-------------------------------------------------------+   |             - Potential URL and cookie length issues    |   |             - Sending large volumes of data             |   +-------------------------------------------------------+   |             Need for State Maintenance                  |   +-------------------------------------------------------+   |             - Need to maintain user state               |   |             - Limited statelessness                     |   +-------------------------------------------------------+   |             Security and Encryption                    |   +-------------------------------------------------------+   |             - Potential vulnerabilities                |   |             - Lack of encryption for payload            |   +-------------------------------------------------------+
复制代码


4.7 小结

尽管 JWT 确实消除了数据库查找,但在这样做的过程中引入了安全问题和其他复杂性,因此在用户会话中使用 JWT 是有风险的。


那么何时可以使用 JWT 呢?


在某些情况下,使用 JWT 可能是有意义的,例如在后端进行服务器对服务器(或微服务对微服务)通信时,一个服务可以生成 JWT 令牌并将其发送给另一个服务以进行授权目的。或者其他特定的场景,比如重置密码,您可以发送一个 JWT 令牌作为一次性、短暂的令牌来验证用户的电子邮件。


如果我不能使用 JWT,还有其他什么选择吗?


解决方案是完全不使用 JWT 来进行会话管理。相反,使用传统但经过实战验证的方法,使数据库查找更高效,速度极快(亚毫秒级),以至于额外的调用不会造成影响。

五、将会话存入 Redis

如果迄今为止我们概述的问题的答案是使用经过验证的方法(即在数据库中存储会话),但要使数据库查找如此快速以至于额外的调用不会有影响,那么如何实现这一点呢?


您需要一个能够在亚毫秒级别内处理数百万请求的数据库。成千上万的公司为了这个确切的目的使用 Redis 服务数十亿的用户。


使用 Redis,额外的数据库调用如此之快,以至于不再成为问题。

5.1 工作方式

  1. 输入用户名和密码进行登录:

  • 服务器首先对用户进行身份验证。

  • 服务器然后在 Redis 中创建一个会话令牌,并将该令牌与用户信息一起存储。存储的形式如下:

  • SET sess:12345 "{user:raja, shopping:3, DOB: 1/1/21}"

  • 其中:

  • 12345 是会话令牌 ID。

  • "sess:12345"是 Redis 键。

  • "{user:raja, shopping:3, DOB: 1/1/21}"是值。

  1. 服务器然后将会话令牌发送给前端的移动应用程序或 Web 应用程序。

  • 该令牌通常存储在应用程序的 Cookie 或本地存储中。

  1. 接下来,假设您像之前的示例一样编写并提交了一条推文。您的应用程序将连同推文一起发送会话令牌(通过 Cookie 或标头),以便服务器可以识别您。但是,与之前一样,令牌只是一个随机字符串,那么服务器如何使用它来识别您呢?

  2. 当服务器收到会话令牌时,它不知道用户是谁,因此将其发送到 Redis 以从该令牌中检索实际用户的信息(例如 userID)。

  • GET sess:12345

  • “{user:raja, shopping:3, DOB: 1/1/21}” // Redis 返回的响应

  1. 如果用户存在并且被允许完成操作(例如发送微博),服务器允许其执行该操作。

  2. 最后,服务器向前端应用程序通知微博已发送(使用原始请求的响应)。


由于 Redis 非常快速,您无需在客户端和服务器之间来回传输大量用户数据,只需一个会话令牌即可。您可以在微秒或亚毫秒级别从 Redis 中获取实际有效载荷。由于不将数据发送给客户端,因此更难被人窃取。


而且由于实际数据位于 Redis 中,您不必依赖会话过期时间,只需将其与 Redis 数据库本身进行验证即可。最后,在用户注销时,您可以从 Redis 中删除用户数据,以确保始终进行身份验证和授权。


正如您所见,这是一种将数据存储在数据库中的古老方法,但通过使其极快,您有效地消除了速度问题。而且它没有像 JWT 那样的安全漏洞。

5.2 代码实例

 package main ​ import (     "fmt"     "log"     "time" ​     "github.com/go-redis/redis/v9" ) ​ func main() {     // 创建Redis客户端     client := redis.NewClient(&redis.Options{         Addr:     "localhost:6379", // Redis服务器地址         Password: "",               // 密码(如果有的话)         DB:       0,                // 使用默认数据库     }) ​     // 确保与Redis服务器的连接正常     pong, err := client.Ping().Result()     if err != nil {         log.Fatal(err)     }     fmt.Println("Connected to Redis:", pong) ​     // 模拟用户登录     user := "raja"     sessionID := "12345"     sessionData := "{user:raja, shopping:3, DOB: 1/1/21}" ​     // 存储会话数据到Redis     err = client.Set("sess:"+sessionID, sessionData, 0).Err()     if err != nil {         log.Fatal(err)     } ​     // 设置会话数据的过期时间     err = client.Expire("sess:"+sessionID, time.Minute).Err()     if err != nil {         log.Fatal(err)     } ​     // 从Redis检索会话数据     result, err := client.Get("sess:" + sessionID).Result()     if err != nil {         log.Fatal(err)     }     fmt.Println("Session data:", result) ​     // 关闭Redis连接     err = client.Close()     if err != nil {         log.Fatal(err)     } }
复制代码


 import redis ​ # 创建Redis客户端 r = redis.Redis(host='localhost', port=6379, db=0) ​ # 模拟用户登录 user = "raja" sessionID = "12345" sessionData = "{user:raja, shopping:3, DOB: 1/1/21}" ​ # 存储会话数据到Redis r.set("sess:" + sessionID, sessionData) ​ # 设置会话数据的过期时间 r.expire("sess:" + sessionID, 60)  # 过期时间为60秒 ​ # 从Redis检索会话数据 result = r.get("sess:" + sessionID) print("Session data:", result) ​ # 关闭Redis连接 r.close()
复制代码

请注意,你需要先在计算机上安装 Go 语言和 Python 的 Redis 库,然后使用上述代码进行编译和运行。另外,确保 Redis 服务器在本地运行,并使用正确的主机和端口。

六、当 Redis 是你的主数据库时

Redis 在过去的十多年中一直是用于缓存和会话存储的领先数据库。但在过去几年中,Redis 已经发展成为一个主要的多模型数据库,提供了七个官方支持的模块。例如,你可以使用 RedisJSON(比市场上的领导者快 10 倍)实现类似实时 MongoDB 的数据库,或者使用 RediSearch 模块(速度提升 4 倍到 100 倍)实现实时全文搜索,类似于 Algolia。


使用 Redis 作为你的主数据库,一切都变得非常快速,不仅仅是会话存储。在这种架构中,所有应用程序的主要数据和会话数据以及其他所有数据都存在同一个数据库中。

  1. 你使用用户名和密码登录: › 服务器首先通过从 Redis 中获取用户信息(而不是从慢速数据库中获取)对用户进行身份验证。

HGET user:01 ii. > username: raja password wer8wrw9werw8wrw // 响应 • 假设用户名和(加密的)密码以哈希映射的形式存储在"user:01"键中。


服务器然后创建一个会话令牌,并将该令牌与用户的信息一起存储到 Redis 中。


它将如下所示:

  • SET sess:12345 "{user:raja, shopping:3, DOB: 1/1/21}"

  • 其中 • 12345 是会话令牌的 ID。 • "session:12345" 是 Redis 键。 • "{user: raja, shopping: 3, DOB: 1/1/21}" 是值。


  1. 服务器然后将会话令牌发送给前端移动应用程序或 Web 应用程序。 › 该令牌随后存储在应用程序的 Cookie 或本地存储中。

  2. 接下来,假设你写了一条推文并提交。然后,你的应用程序会将会话令牌(通过 Cookie 或标头)与推文一起发送给服务器,以便服务器可以识别你是谁。但令牌只是一个随机字符串,那么服务器如何通过会话令牌知道你是谁呢?

  3. 当服务器接收到会话令牌时,它不知道用户是谁,因此将其发送给 Redis 以从该令牌中检索(4a)实际用户的信息(如 userID)。 › GET session:12345 › > "{user:raja, shopping:3, DOB: 1/1/21}" // 从 Redis 获取的响应

  4. 如果用户存在并被允许执行该操作(例如发送推文),服务器允许他们完成该操作。

  5. 最后,服务器通知前端推文已发送成功。

七、同时使用 JWT 和 Redis

在这最后一章中,我们将回顾另一种广泛使用的选项,它提供了一些 JWT 的好处,但消除了之前讨论的大部分安全问题(除了中间人攻击)。可以在使用 Redis 作为次要检查的同时,使用 JWT 作为初步检查。在这种情况下,如果 JWT 验证成功,服务器仍然会访问 Redis 并在那里进行双重检查。然而,如果 JWT 验证本身失败,就不需要担心检查 Redis 数据库了。


这种方法的另一个好处是,你可以在前端和后端都使用现有的 JWT 库,而不需要开发自己的自定义方式来存储数据在 Redis 中(尽管这并不是什么大问题)。


还有一点需要注意的是,正如前面提到的,这个设置仍然会使应用程序容易受到潜在的中间人攻击,因为令牌没有加密。


正如之前提到的,有一种加密 JWT 令牌的方法叫做 JWE,但是当你使用这种方法时,客户端(特别是浏览器和移动设备)无法解密它们以查看实际的有效载荷。此时,你从 Web 应用程序和移动应用程序的角度来看,实际上是使用 JWT 作为常规的加密会话令牌。


如果你将其用于机器之间的通信,例如在微服务中希望在两个不同的服务之间共享登录信息时,可以共享公钥来解密和查看 JWT 数据,但这是一个不同的用例。

具体实现可能因库而异,但通常情况下,该过程如下所示:

  1. 用户使用用户名和密码登录。

  • a. 服务器验证用户名和密码是否有效。

  • b. 服务器创建一个 JWT 令牌(而不仅仅是普通的会话令牌)。

  • c. 服务器将 JWT 令牌发送到 Redis 进行存储。

  1. 服务器将 JWT 令牌发送回客户端。


用户发送 wb:

  1. 用户发送 wb(同时带上 JWT 令牌)。

  2. 服务器首先验证 JWT 令牌的有效性。

  3. 如果 JWT 令牌有效,服务器会询问 Redis 该 JWT 令牌是否存在。

  4. Redis 告诉服务器令牌是否仍然存在(我们不需要进行任何其他验证;只需检查键是否存在就足够了,因为 JWT 已经完成了这个工作)。

  5. 如果键存在,服务器将推文保存到数据库中。

  6. 服务器向客户端发送响应,告知 wb 已保存。


用户注销:

  1. 用户注销。

  2. 服务器立即在 Redis 中删除令牌。因此,从现在开始,步骤 7 将失败。

  3. 服务器向客户端发送成功的注销响应。

7.1 代码实现

 package main ​ import (     "fmt"     "log" ​     "github.com/go-redis/redis/v8" ) ​ func main() {     // 创建Redis客户端     rdb := redis.NewClient(&redis.Options{         Addr:     "localhost:6379", // Redis服务器地址和端口         Password: "",               // 密码(如果有)         DB:       0,                // 使用的数据库     }) ​     // 测试连接     pong, err := rdb.Ping(ctx).Result()     if err != nil {         log.Fatal(err)     }     fmt.Println("Redis连接成功:", pong) ​     // 用户登录     username := "raja"     password := "password123" ​     // 验证用户名和密码(此处省略实际验证过程) ​     // 创建JWT令牌     jwtToken := "your_jwt_token" ​     // 存储JWT令牌到Redis     err = rdb.Set(ctx, "sess:"+jwtToken, "{user:raja, shopping:3, DOB: 1/1/21}", 0).Err()     if err != nil {         log.Fatal(err)     } ​     // 发送JWT令牌给客户端     // 这里可以将JWT令牌发送给客户端,例如作为响应的一部分或存储在Cookie中 ​     // 用户发送微博     tweet := "Hello, World!" ​     // 验证JWT令牌的有效性(此处省略实际验证过程) ​     // 检查JWT令牌是否存在于Redis中     tokenExists, err := rdb.Exists(ctx, "sess:"+jwtToken).Result()     if err != nil {         log.Fatal(err)     } ​     if tokenExists == 1 {         // 保存微博到数据库(此处省略实际保存过程) ​         // 发送成功响应给客户端         fmt.Println("微博保存成功")     } else {         // 发送无效令牌错误给客户端         fmt.Println("JWT令牌无效")     } ​     // 用户注销     // 在注销操作中,从Redis中删除JWT令牌,使其无效 ​     err = rdb.Del(ctx, "sess:"+jwtToken).Err()     if err != nil {         log.Fatal(err)     } ​     // 发送成功注销响应给客户端     fmt.Println("用户注销成功") }
复制代码


 import redis ​ # 创建Redis客户端 rdb = redis.Redis(host='localhost', port=6379, password='', db=0) ​ # 用户登录 username = "raja" password = "password123" ​ # 验证用户名和密码(此处省略实际验证过程) ​ # 创建JWT令牌 jwt_token = "your_jwt_token" ​ # 存储JWT令牌到Redis rdb.set("sess:" + jwt_token, "{user:raja, shopping:3, DOB: 1/1/21}") ​ # 发送JWT令牌给客户端 # 这里可以将JWT令牌发送给客户端,例如作为响应的一部分或存储在Cookie中 ​ # 用户发送推文 tweet = "Hello, World!" ​ # 验证JWT令牌的有效性(此处省略实际验证过程) ​ # 检查JWT令牌是否存在于Redis中 token_exists = rdb.exists("sess:" + jwt_token) ​ if token_exists:     # 保存微博到数据库(此处省略实际保存过程) ​     # 发送成功响应给客户端     print("微博保存成功") else:     # 发送无效令牌错误给客户端     print("JWT令牌无效") ​ # 用户注销 # 在注销操作中,从Redis中删除JWT令牌,使其无效 ​ rdb.delete("sess:" + jwt_token) ​ # 发送成功注销响应给客户端 print("用户注销成功")
复制代码


作者:POGF

链接:https://juejin.cn/post/7238921098808475705

来源:稀土掘金

用户头像

还未添加个人签名 2021-07-28 加入

公众号:该用户快成仙了

评论

发布
暂无评论
JWT真的安全吗?如何解决该问题_JWT_做梦都在改BUG_InfoQ写作社区