JWT 身份验证最佳实践
基于 JWT 的身份验证
最后的想法
在设计可扩展且易于扩展的架构时,微服务是一个绝佳的工具。它们能够以其他许多架构范式无法企及的方式封装不同的行为或职责。
如果将它们与基于 REST 的接口相结合,那么您不仅可以编写和创建一个能够自动扩展的平台(当然,前提是拥有合适的基础架构),还可以创建一个标准化且易于使用的产品。
如果您还没有注意到,我是微服务的粉丝,它们通常是我在设计新架构、日常处理大数据时采用的模式,我倾向于需要开箱即用的灵活性和可扩展性,它们为我提供了这些。
然而,在编写微服务时,并非每个人都会考虑到需要一种身份验证的方式。无论您是使用前端客户端,还是仅仅通过另一个微服务与它们通信,都需要一种身份验证方式。虽然市面上有多种身份验证方案,但我想介绍一种最简单但最强大的替代方案:JSON Web Tokens。
基于 JWT 的身份验证
要理解基于 JWT 的身份验证,你需要知道的基本知识是,你处理的是一个加密的 JSON,我们称之为“令牌”。这个令牌包含了后端系统识别你身份所需的所有信息,以及你是否确实如你所说的那样。
下图显示了此过程所涉及的步骤:
如您所见,省去基于用户的步骤,您只需要 4 个步骤:
- 首先,客户端应用程序(这里我使用了前端应用程序,但您也可以使用其他服务执行相同的操作)将发送登录请求。这意味着您只需发送一次登录凭据。
- 其次,API 会验证这些凭证,如果正确,就会生成 token。这是最重要的一步,因为正如我之前提到的,生成的 token 只是一个加密的 JSON 对象。你可以根据需要添加任意数量的数据,而之所以需要添加数据,是因为 JWT 允许你执行无状态授权,我稍后会详细介绍。
- 第三,生成 JWT 后,您要做的就是将其返回给客户端应用程序。
- 最后,客户端应用稍后会在每次后续请求中发送此令牌。此令牌表示您已通过身份验证,可以访问应用程序的机密部分。
就是这样,流程非常简单,你无需将用户重定向到任何地方(我正在看 OAuth!)。
但让我们更详细地讲解一下,让我分解每个步骤,以便你充分理解代码背后发生的事情。
后端方面
对于后端或微服务,您需要了解两个主要步骤:
- 生成 JSON Web Token。这一点很关键,正如我之前提到的,因为你添加的信息稍后会用到(就像说“你说的每一句话都会在法庭上成为对你不利的证据”一样)。
- 验证收到的请求的令牌。我把这部分从身份验证流程中省略了,因为这实际上是授权流程的一部分。非常相似,并且易于实现,但也值得注意。
那么,让我们开始吧。
生成 JWT
要在后端微服务上生成令牌,通常需要使用现有的服务器端库。您无需了解令牌的生成过程,只需了解其中的内容即可。
那么,令牌中究竟包含什么呢?您可以使用 JSON 对象,例如:
{
"foo": "bar"
}
这些数据将被使用并返回给前端客户端,这可能符合你的业务逻辑,也可能你的前端客户端正在等待“foo”键。然而,除了你可以添加的自定义属性之外,还有一些预定义的选项,它们对于库所使用的特定算法具有功能意义。
鉴于我将使用Node.js 的jsonwebtoken库,您需要考虑的主要选项是expiresIn
。这对于生成正确的 JWT 至关重要,因为您希望令牌具有有效期。否则,它将永远存在,可能会给那些可以捕获它并随后使用它来冒充您的身份的人留下一个漏洞。
对于这个特定的库,如果您提供一个数字,则该值以秒为单位(或者,您可以提供一个使用时间单位的字符串,例如"
2 days
"
表示 2 天的有效期)。
反过来,该库将添加另一个代表“Issued At”iat
的日期参考,用于到期检查(即,在检查您的令牌是否仍然有效时将考虑该日期)。
那么如何将所有这些信息添加到令牌中呢?通过签名:
const jwt = require('jsonwebtoken');
const token = jwt.sign({
data: 'foobar'
}, 'your-secret-key-here', { expiresIn: 60 * 60 }); //1 hour
console.log(token)
//Outputs: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vYmFyIiwiaWF0IjoxNjA2NTUzNjc4LCJleHAiOjE2MDY1NTcyNzh9.3oV51hA7SKCqDhG7tbu2XQq7wW2LJE5XtK0HhVwEs0A
使用该sign
方法可以创建令牌。请注意,您的主对象(包含要传输到前端的实际信息)是第一个参数,第二个参数是密钥或短语(您可以传递您选择的短语,但必须与客户端共享)或 PEM 密钥的内容。无论哪种方式,第二个参数都用于加密算法来编码和创建令牌。最后,第三个属性包含配置选项(在我们的例子中只有到期时间)。
然后,此令牌(注意上面代码的输出)作为身份验证响应的一部分返回,供客户端使用。
存储令牌
作为可选步骤,您还可以将令牌存储在数据库中,以将其与您的用户关联。通常,如果所有用户信息都可以存储在令牌中,则无需执行此操作。
但是,如果有更多信息需要管理而您可以轻松地存储在令牌中,那么在数据库中保持与用户个人资料的关联可能是个好主意。
事实上,考虑到查找此令牌是您在每个请求中都会做的事情,一个好的选择是将令牌和有关用户的相关信息都保存在内存存储器中,比如Redis。
包含存储和验证支持的新流程如下:
这里最费力的交互不是与 Redis 的第一个交互(#4),而是第二个交互(#9),因为每次收到请求时都会发生这种情况。我们稍后会详细介绍。
检查令牌。
虽然我们在请求中获取了令牌,但这并不意味着该请求是安全的,它很可能是伪造的,或者包含无效甚至过期的令牌。因此,每次请求安全资源(例如,需要经过身份验证的用户才能访问的端点,或位于会员区内的网站部分)时,都需要验证收到的令牌。
如果您跳过了存储步骤,那么这项任务相对简单。您只需使用相同的服务器端框架进行验证即可:
const decodedToken = jwt.verify(token, 'your-secret-key-here')
console.log(decodedToken)
注意,我使用的是同一个“密码短语”,这一点非常重要,因为你需要在同一个项目中一直使用同一个短语,否则验证将无法进行。
过期的令牌会抛出如下异常:
有效的 JSON 只会返回有效的 JSON,您可以根据需要对其进行解码和使用。
{ data: 'foobar', iat: 1606581962, exp: 1606581963 }
注意库添加的iat
和参数。 在这种情况下,异常意味着您需要使客户端的请求无效并发送无效的响应。通常情况下,您会返回 403 错误代码,因为该请求(以及客户端)不再经过身份验证。exp
SPA认证
现在我们理解了 API(或者说微服务)受 JWT 身份验证流程保护的含义,我想从充当客户端应用的 SPA 应用的视角来介绍相同的流程。
在这种情况下,正如我之前提到的,您最初将通过发送凭据并接收令牌来联系服务,之后每次请求都必须使用该令牌。
不过,我们首先需要了解的是,基于会话的身份验证与基于令牌的身份验证不同。
基于会话的身份验证 vs. 基于令牌的身份验证
乍一看,这两种策略可能看起来很相似,所以我想介绍一下它们的区别。
本质上,这两种方法的工作原理相同:
- 您针对服务进行身份验证。
- 该服务验证你的凭证并发回一个令牌
- 在每个后续请求中,您都会发送该令牌来向服务验证您的身份。
所以正如您所看到的,过程和数据流看起来是相同的,但隐藏着一些主要的差异。
- 对于基于会话的令牌,服务器会返回一个会话密钥,该密钥引用会话数据(所有与您作为登录用户相关的数据)。然而,这些数据保存在服务器的内存中。这实际上破坏了 RESTful API 的优势之一:无状态服务可以轻松扩展,因为内存中不存储任何会话信息。您会看到,一旦您使用将会话信息保存在内存中的服务器登录,您发送的每个后续请求都需要发送到该服务器(因为内存无法在不同的服务器之间共享,或者至少不容易共享)。如果您尝试扩展架构以处理更多流量,那么复制服务来增加容量将不像使用无状态服务那样简单。
- 基于会话的身份验证将会话密钥存储在浏览器的 Cookie 中。它们将信息以 Cookie 的形式发送,因此,当微服务来自不同的域时,浏览器会遇到问题。对于基于令牌的身份验证来说,这不是问题,因为令牌是作为请求标头发送的。
本质上,这两个巨大的差异使得基于令牌的身份验证更加可行,并且是我必须执行的首选方法。
使用 JWT 应对 XSS 和 CSRF 问题
话虽如此,在实施身份验证策略时,您还需要考虑其他实际的安全威胁。
这一点至关重要,因为您需要谨慎处理代码中的 JWT。这两种攻击都可能利用您的漏洞,将您的令牌用于恶意目的。
XSS 或跨站点脚本意味着攻击者以某种方式将恶意代码注入前端,从而影响应用程序的所有用户。当平台使用用户输入而不对其进行验证甚至编码时,就可以实现这一点。想象一下在主页上列出所有登录的用户名,如果您没有对每个用户名的输入文本进行编码和检查,攻击者可能会输入 JavaScript 脚本而不是实际用户名,导致您的主页在每个用户的笔记本电脑上执行该脚本。如果您使用 JavaScript 代码手动处理令牌,这绝对是一个有问题的攻击媒介。
这就是为什么您不使用本地存储在前端存储 JWT 的原因。LS 对存储在同一域中的任何 JS 代码都是公开访问的,因此,如果攻击者能够像我提到的那样注入代码,那么每个人的令牌都是可访问的。您不想使用本地存储来保存令牌。记住这一点。
CSRF(跨站请求伪造)是指攻击者利用每个请求(即使是跨域请求)都可以发送 Cookie 的漏洞。如果您作为用户被诱骗点击恶意链接,最终可能会被一个网站发送请求,甚至可能是更改密码。由于您之前曾登录过安全网站,并且浏览器仍然存储着 Cookie,因此请求将会成功。所以也不要将令牌存储在 Cookie 中!
等等,那还剩下什么?没有本地存储,没有 Cookie,或许可以放在内存里?这当然是个选择,除非你不希望用户在刷新浏览器时丢失会话。
与其因为安全选项不足而抓狂,不如先回到 Cookie 上来。
如果你正确创建了 Cookie,它们就足够安全,值得你信赖,再加上一些额外的检查,你就能提供一个相对安全的流程,让我来解释一下:
- httpOnly:此选项确保 Cookie 无法通过 JavaScript 访问。创建 Cookie 时,这绝对是必须的,因为您不需要(也不希望)通过用户代码访问它们。
- SameSite策略:正如我之前提到的,每个请求都会发送 Cookie,除非浏览器是新版本(例如,您使用的不是 Internet Explorer 8 或更早的版本),并且 Cookie 的 SameSite 策略为空。如果您需要将令牌发送到不同的域名,那么Lax值就可以满足您的需求,因为它允许您向其他域名发送 GET 请求,但无法发送 POST 请求。这很棒,因为您可以选择进行重定向,而恶意代码无法在您背后发送 POST 请求。
这两个选项都很棒,但并非完美。如果您真的想找到一种方法来让您的应用程序 100% 安全,那么您可能需要做出一些妥协,例如禁用跨域请求,这些都是您必须根据实际安全需求进行的安全分析的一部分。
话虽如此,尽管所有这些安全考虑都很重要,但你通常会使用一个已经为你处理好所有问题的库。例如,如果你在 Vue 项目中使用 axios 进行 HTTP 请求,则只需在文件中设置一行配置即可main.js
:
axios.defaults.withCredentials = true
有了这行代码,您就无需担心在整个过程中处理令牌甚至 Cookie 了。只要后端 API 正确创建了 Cookie,您就会在每个后续请求中发送它。您可以在此处访问与 JWT 身份验证 API 交互的示例项目的完整代码。
是否需要使用 HTTPS 和 JWT?
在客户端-服务器通信中,这是一个非常常见的问题,尤其是在设置基于 JWT 的身份验证时,因为人们往往认为只要有了令牌,就可以了。但
事实是,正如我上面提到的,实现 100% 的安全不仅困难,而且几乎不可能。如果有人足够熟练地想要入侵你的系统,他们总能找到办法。本文旨在通过基本步骤阻止 90% 的潜在攻击者。
那么,HTTPS 是否是必需的呢?让我们来思考一下 HTTPS 的真正含义:
客户端与服务器端服务之间的 HTTPS 连接是两端之间加密的通信通道。攻击者几乎无法从外部读取该数据流并获知发送的内容。因此,与未加密的普通 HTTP 不同,HTTPS 确保只有您正确识别为有效的客户端才能与您的服务通信。
您通常希望做的是将您的公共部分放在普通 HTTP 中,而任何需要保护的内容(即需要您登录才能访问的内容)都将放在 HTTPS 后面。
何时不使用 JWT?
我总是说,技术没有灵丹妙药。每个促使你做出特定选择的理由,都可能让你走向相反的方向。
基于 JWT 的身份验证也是如此,许多开发者声称有更好、更安全的选择。关键在于正确定义“更好”的含义,并坚持下去。
毕竟,你的上下文是所有决策的决定性因素。
话虽如此,一个不使用基于 JWT 的身份验证的很好的理由是当你开发有状态的后端时。毕竟,考虑到数据流的无状态特性,JWT 在用于保护无状态 API 时非常出色。当然,
它也可以在需要状态的情况下使用,但这样一来,JWT 和会话令牌之间的界限就变得非常模糊了。在这种情况下,选择其他替代方案(例如最初为此设计的服务器端会话)可能是更好的选择。
最佳 JWT 库
我最后想介绍的是库。如果你在后端工作,则不需要任何与 Vue 兼容的特定库,但对于本文中的示例,我使用了 Node.js 的jsonwebtoken服务器端库。
至于前端,正如我之前提到的,axios 已经完全能够提供与 JWT 认证 API 交互所需的所有用户级支持。但是,如果您实际上希望执行更高级的任务,例如手动解码令牌以使用其内部信息,则可以根据您选择的前端框架选择一些选项:
- 对于 VueJS 用户:您可以使用类似vuejs-jwt这样的轻量级库,它提供了诸如
decode
或hasToken
之类的方法,帮助您直接处理令牌。另一方面,如果您正在寻找更强大的解决方案,您还可以考虑vue-auth,它支持多种身份验证算法,当然也包括 JWT。 - 对于 Angular 用户:您可以使用 Auth0 的库:angular-jwt。它为您提供了一个简单的界面来与令牌进行交互(即获取其数据、检查到期日期等)。
- React 用户:最后,如果您是“其中之一”(实际上,只是开玩笑,我是 React 的忠实粉丝),您可以使用react-jwt,它也提供了一个
useJwt
钩子,允许您对令牌有效性的变化做出反应(您可以直接在前端检查这一点,并立即对到期事件做出反应)。
最后的想法
当谈到 RESTful API(或任何无状态后端服务)的安全时,基于 JWT 的方法绝对是一个不错的选择。默认情况下,它可能不是最安全的,但只需稍加修改和配置,就可以应对大多数常见的攻击向量。
它能够很好地控制令牌的使用方式和生成方式,并使后端基础设施的扩展变得非常简单。
最终,这取决于你的情况和背景。对我来说,JWT 身份验证一直是一个不错的选择,而且我也会尽力推广它,因为我也是 RESTful 微服务的忠实粉丝。
你呢?你以前用过 JSON Web Tokens 吗?或者你更喜欢其他方法?请在下方留言分享你的经验。
前端监控
在生产环境中调试 Web 应用可能既困难又耗时。Asayer 是一款前端监控工具,可以重放用户的所有操作,并展示应用在遇到每个问题时的表现。这就像打开浏览器的检查器,同时监控用户的运行情况。
Asayer 可让您重现问题、汇总 JS 错误并监控应用性能。Asayer 提供用于捕获Redux 或 VueX存储状态以及检查Fetch请求和GraphQL查询的插件。
对于现代前端团队来说,快乐调试 -开始免费监控您的网络应用程序。
文章来源:https://dev.to/deleteman123/jwt-authentication-best-practices-3lf9