构建安全应用程序的顶级身份验证技术

来自程序员技术小站
Admin留言 | 贡献2026年2月7日 (六) 14:31的版本
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)
跳转到导航 跳转到搜索

身份验证是确保应用程序及其处理的敏感数据安全的第一道防线。无论是个人银行应用程序、企业平台还是电子商务网站,都需要有效的身份验证机制来验证用户身份并保障其对资源的访问权限。

如果没有适当的身份验证,应用程序容易受到未经授权的访问、数据泄露和恶意攻击,可能导致重大的经济损失、声誉损害和隐私侵犯。

除了安全性之外,身份验证在用户体验中也扮演着至关重要的角色。通过识别用户,应用程序可以提供个性化服务、记住用户偏好,并启用跨平台的单点登录 (SSO) 等功能。

随着威胁的不断演变,实现安全高效的身份验证比以往任何时候都更具挑战性。开发人员必须在相互冲突的优先事项之间寻求平衡,例如安全性(确保抵御不同类型的攻击,例如会话劫持、令牌窃取和重放攻击)、可扩展性(在不影响性能的前提下支持数百万用户)和用户体验(在应用强大的安全措施的同时保持易用性)。

为了应对这些挑战,开发人员依赖于各种身份验证技术。在本文中,我们将探讨应用程序中使用的多种身份验证技术,并了解它们的优缺点。

身份验证基础知识

身份验证是指验证尝试访问应用程序的用户、设备或系统的身份的过程。简单来说,就是应用程序如何确保访问者或系统的身份与其声称的身份相符。它通常涉及验证用户名、密码、生物识别数据或令牌等凭证。

例如,当我们使用密码登录网站时,应用程序会将我们输入的密码与存储的凭据进行比较,以确认我们的身份。在基于 API 的系统中,应用程序可能会使用令牌来验证调用服务是否有权与后端交互。

身份验证与授权

身份验证和授权是密切相关但又不同的过程。

身份验证回答“你是谁?”这个问题,侧重于验证身份。授权则回答“你被允许做什么?”这个问题,通过确定授予已验证用户的权限或访问级别来实现。

例如,身份验证用于确认我们是否是电子商务平台的注册用户。授权则决定我们是否可以查看订单历史记录或以管理员身份管理库存。换句话说,身份验证用于确认身份,而授权则基于该身份实施访问控制。

接下来,我们将探讨 cookie 和会话在身份验证方面的应用:

Cookie 是由 Web 服务器存储在客户端浏览器上的小型数据文件。它们在原本无状态的 HTTP 通信中扮演着关键角色,使 Web 应用程序能够在多个请求中记住信息,从而维护通信状态。

Cookie 允许 Web 服务器存储请求之间持久存在的数据,使其可用于各种用途,例如会话管理(使用会话 ID 跟踪已登录用户)、个性化(存储用户偏好或设置)以及分析和跟踪(记录用户行为以进行分析或定向广告)。

在身份验证中,Cookie 通常用于存储会话令牌或标识符,以验证用户的身份。

该过程通常包含两个部分。用户登录时,用户提供用户名和密码等凭据。服务器验证这些凭据并生成会话 ID 或令牌。此会话 ID 会发送到浏览器并存储在 cookie 中。

对于后续请求,浏览器会自动将 cookie 添加到每个 HTTP 请求的 Cookie 标头中。服务器会从 cookie 中读取会话 ID,对其进行验证,并识别用户。

请看下图:

Cookie 具有多种属性,用于控制其行为和安全性:

  • HttpOnly 属性可阻止 JavaScript 访问 cookie,从而缓解跨站脚本 (XSS) 攻击。Secure 属性可确保 cookie 仅通过 HTTPS 连接发送,从而防止中间人攻击。
  • SameSite 属性有助于防止跨站请求伪造 (CSRF) 攻击。它有三个值:Strict(cookie 永远不会在跨站请求中发送)、Lax(cookie 仅在顶级导航中发送)和 None(cookie 随所有请求发送,但需要 Secure 属性)。
  • Domain 属性指定哪些主机可以接收 cookie。例如,设置 Domain= example.com允许将 cookie 发送到example.com及其所有子域。Path 属性将 cookie 限制在特定的 URL 路径中。
  • 对于过期时间,我们可以使用 Max-Age(指定有效期,单位为秒)或 Expires(设置绝对过期日期)。如果两者都未设置,则该 cookie 将成为会话 cookie,并在浏览器关闭时被删除。

第一方 Cookie 由我们当前访问的域名设置。

当我们访问 example[dot]com 时,如果该网站设置了 cookie,那就是第一方 cookie。这些 cookie 用于网站的基本功能,例如身份验证和用户偏好设置。

第三方 Cookie 由与我们正在访问的网站不同的域名设置。例如,如果我们访问 example[dot]com,而该网站加载了来自ads.com的广告,则ads.com域名可以设置一个 Cookie。这些 Cookie 通常用于跨多个网站跟踪用户行为,以达到广告投放的目的。

出于隐私考虑,现代浏览器对第三方 Cookie 的限制日益增多。有些浏览器甚至默认阻止第三方 Cookie。为了确保身份验证在所有浏览器上都能可靠运行,我们应该始终使用第一方 Cookie。

Cookie面临多项安全挑战:

  • 如果恶意脚本通过跨站脚本攻击 (XSS) 注入到网站中,它就可以访问包含敏感数据的 cookie。设置 HttpOnly 标志可以防止这种情况发生。
  • 如果 Cookie 通过未加密的 HTTP 协议传输,则可能在中间人攻击中被拦截。“安全”标志可确保 Cookie 仅通过 HTTPS 协议发送。
  • 请求中自动发送的 Cookie 可能被 CSRF 攻击利用来执行未经授权的操作。实施 CSRF 令牌并使用 SameSite 属性有助于缓解这种情况。
  • 浏览器对 Cookie 的大小有限制,通常每个 Cookie 为 4KB,并且每个域的 Cookie 数量也有限制。过多的 Cookie 会导致性能问题或请求被拒绝。

以下是一些安全处理 Cookie 的最佳实践:

  • 我们应该始终设置 HttpOnly 标志来阻止 JavaScript 访问并降低 XSS 风险。Secure 标志可确保 cookie 仅通过 HTTPS 发送。
  • 使用 SameSite 属性(SameSite=Strict 或 SameSite=Lax)可以限制跨站点请求并降低 CSRF 风险。
  • 我们应该对敏感数据使用较短的过期时间,避免在 cookie 中存储密码或信用卡号等敏感信息,而只存储令牌或标识符。

会话

会话是一种服务器端机制,用于在用户与应用程序进行交互期间存储和管理用户身份验证数据。与将数据存储在客户端的 Cookie 不同,会话将数据安全地保存在服务器端,客户端仅持有一个引用,通常是会话 ID。

使用会话进行身份验证的流程包括以下几个步骤:

  • 用户登录时,用户提交登录凭据。服务器验证凭据并为用户创建一个会话,通常用唯一的会话 ID 表示。
  • 为了生成会话 ID,服务器会创建一个与存储在服务器上的用户会话数据关联的随机唯一标识符。此会话 ID 会通过 cookie 或其他传输机制发送给客户端。
  • 服务器将会话数据(例如用户 ID、角色和偏好)存储在内存、数据库或其他存储系统中。
  • 会话 ID 充当检索此数据的密钥。对于每个后续请求,客户端都会发送会话 ID,通常通过 cookie 发送。服务器验证会话 ID,检索关联数据,并使用它来处理请求。

请看下图:


会话可以设置生存时间 (TTL),在一段时间不活动或达到最大持续时间后自动过期。当用户注销或出于安全考虑撤销会话时,会话数据将从服务器中删除,会话 ID 也将失效。

Cookie 和会话通常协同工作。

Cookie 存储着客户端的会话 ID。每次请求时,浏览器都会将 cookie 发送到服务器,以便服务器识别用户的会话。

以下是一个简单的代码示例,展示了如何将会话 ID 设置到 cookie 中:

 app.post ('/login', (req, res) => { const sessionId = createSession(req.body.username); res.cookie('session_id', sessionId, { httpOnly: true, secure: true, maxAge: 3600000 }); res.send('Login successful'); });

对于传入的请求,服务器可以读取存储在 cookie 中的会话 ID:

 app.get('/profile', (req, res) => { const sessionId = req.cookies['session_id']; if (isValidSession(sessionId)) { res.send('Profile page'); } else { res.status(401).send('Unauthorized'); } }); 

会话存储选项

选择合适的会话存储机制对于性能和可扩展性至关重要。

  • 内存存储将会话保存在服务器的 RAM 中,从而提供最快的性能。但是,它不适用于生产环境,因为服务器重启时会话数据会丢失。此外,它还将应用程序限制在单个服务器上,因此不适用于分布式系统。
  • 数据库存储使用关系型数据库(例如 PostgreSQL 或 MySQL)或 NoSQL 数据库(例如 MongoDB)。这种方法提供持久可靠的会话存储。但是,我们需要考虑性能影响,并确保对会话 ID 列进行适当的索引以实现快速查找。数据库存储虽然可靠,但速度可能比基于缓存的解决方案慢。
  • 分布式缓存系统(例如 Redis 或 Memcached)是专为会话管理而构建的高性能键值存储系统。它们提供卓越的性能、内置的生存时间 (TTL) 支持(用于自动会话过期)以及横向扩展能力。由于其速度和可靠性,Redis 在生产环境中尤其适用于会话存储。

内存存储仅应用于开发或测试。对于单服务器的生产应用,数据库存储就足够了。对于分布式系统或需要高性能和可扩展性的应用,建议选择 Redis 或 Memcached。

分布式会话策略

当在负载均衡器后运行多个服务器时,我们面临的挑战是如何确保所有服务器都能访问相同的会话数据。

粘性会话(也称会话亲和性)是一种负载均衡方法,它将来自特定用户的所有请求路由到同一台服务器。这种方法易于实现,因为会话无需在服务器之间共享。然而,它可能导致负载分布不均,并且如果服务器发生故障,用户的会话将会丢失。

请看下图:

会话复制是指将会话数据复制到集群中的所有服务器。这提供了高可用性,因为即使一台服务器发生故障,其他服务器也能获取会话数据。然而,由于会话需要持续同步,这会带来显著的网络开销,而且在大型集群中维护跨服务器的一致性可能极具挑战性。

集中式会话存储使用单一数据源(通常是 Redis 集群),所有服务器都从该数据源读取和写入会话数据。这种方法具有高度可扩展性,并能确保所有服务器之间的数据一致性。主要问题在于会话存储可能成为单点故障,但可以通过使用 Redis 集群或复制来缓解。这种策略推荐用于微服务架构。

使用会话的挑战

在微服务等分布式系统中,会话数据必须在服务器之间共享,这就需要额外的基础设施,例如集中式数据库或粘性会话。这些解决方案会增加复杂性并可能造成瓶颈。

此外,如果攻击者窃取了会话 ID,他们就有可能冒充用户。可以通过使用 HTTPS 加密传输中的数据并实施会话超时来缓解这种情况。

大量的活跃会话会消耗大量的服务器资源,不过这可以通过使用像 Redis 这样的高效会话存储来解决。

使用 JWT 进行身份验证

JSON Web Tokens (JWT) 是一种紧凑、URL 安全、自包含的令牌格式,用于在各方之间安全地传输信息。

JWT 是无状态的,这意味着服务器无需存储会话数据。所有用户相关信息都包含在令牌中。服务器只需要签名密钥即可验证令牌,这使得 JWT 非常适合多个服务交互的分布式系统。

这就是为什么 JWT 经常被用于身份验证和授权,以实现无状态和可扩展的系统。

JWT的结构

JWT 由三部分组成:头部、有效载荷和签名。每一部分都采用 Base64 编码。

头部包含令牌的元数据,包括令牌类型(JWT)和签名算法,例如 HS256 或 RS256。以下是一个示例:

 { “alg”: “HS256”, “typ”: “JWT” }

有效负载包含声明,这些声明是关于用户或令牌的陈述。声明可以是已注册声明(预定义,例如 iss 表示发行者,exp 表示过期时间,sub 表示主体),公共声明(应用程序定义的自定义声明),或私有声明(发行者和消费者共享的自定义声明)。以下是一个示例:

 { “sub”: “1234567890”, “name”: “John Doe”, “admin”: true, “iat”: 1516239022 }

最后,签名确保令牌的完整性并验证发送者的真实性。它是通过对头部和有效负载进行编码,并使用密钥或私钥对其进行签名而生成的:

 HMACSHA256( base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret )

以下是 JWT 在典型身份验证流程中的工作原理:

  • 用户登录后,服务器验证凭据并生成包含用户声明(例如用户 ID 和角色)的 JWT。
  • JWT 使用密钥(对称签名)或私钥(非对称签名)进行签名。签名后的令牌通常通过响应体或 cookie 发送给客户端。
  • 对于后续请求,客户端会将 JWT 作为 Bearer 令牌包含在 Authorization 标头中。
  • 服务器通过验证签名和检查过期时间 (exp) 或受众 (aud) 等声明来验证令牌。
  • 如果令牌有效,服务器将允许访问所请求的资源。

请看下图:

刷新令牌流

JWT 的一个常见挑战是,出于安全考虑,访问令牌的有效期应该很短,通常为 15 分钟到 1 小时。然而,强制用户反复登录会造成糟糕的用户体验。

刷新令牌通过实现无缝重新身份验证来解决这个问题,无需用户再次输入凭据。该模式使用两种类型的令牌。

  • 访问令牌有效期很短,用于验证 API 请求。
  • 刷新令牌的有效期很长(数天到数周),仅用于获取新的访问令牌。刷新令牌通常安全地存储在 HttpOnly Cookie 或移动设备的安全存储中,以防止被盗。

完整的流程如下:

  • 用户使用凭据登录,服务器验证凭据。
  • 服务器会颁发访问令牌和刷新令牌。
  • 客户端安全地存储了这两个令牌。
  • 对于后续请求,客户端使用 Authorization 标头中的访问令牌。
  • 当访问令牌过期时,客户端会向刷新端点发送带有刷新令牌的请求。
  • 服务器验证刷新令牌,检查其是否仍然有效且未被撤销。
  • 如果有效,服务器将颁发新的访问令牌(并可选择颁发新的刷新令牌)。
  • 客户端使用新的访问令牌进行后续请求。

这种方法兼顾了安全性和用户体验。有效期较短的访问令牌可以在被盗时最大限度地减少损失,而刷新令牌则允许用户保持身份验证状态,无需不断重新输入凭据。

PASETO

PASETO,即平台无关安全令牌(Platform-Agnostic Security Tokens)的缩写,是 JWT 的一种现代替代方案,其设计重点在于安全性、简洁性和加密最佳实践。

PASETO 解决了与 JWT 相关的一些常见漏洞和滥用问题,同时保持了各种身份验证和授权场景所需的灵活性。

与允许灵活选择算法(有时会导致不安全的配置)的 JWT 不同,PASETO 强制使用强大且密码学上可靠的算法,从而降低了因配置错误而导致漏洞的风险。JWT 允许开发者使用像 HS256 或臭名昭著的 none 算法这样的弱算法,而 PASETO 则将选择限制在像 AES-GCM(用于对称加密)和 Ed25519(用于非对称签名)这样强大的算法上。

PASETO 令牌分为本地令牌(加密)和公共令牌(签名),确保敏感数据要么经过安全加密,要么经过签名验证。相比之下,JWT 令牌即使经过签名也始终以明文形式存在,因此容易发生意外的信息泄露。

PASETO 的结构

PASETO 令牌由三到四个主要部分组成,具体取决于它是本地令牌还是公共令牌。这些部分之间用点号分隔。

  • 版本号表示所使用的 PASETO 协议版本。例如,v1 版本使用较旧的加密标准,而 v2 版本使用现代、安全的加密标准。
  • 用途指定令牌的类型,即是否需要加密或签名。选项包括本地(加密令牌)和公共(签名令牌)。
  • 有效载荷包含令牌内的实际数据或声明。对于本地令牌,有效载荷经过加密,并以不透明的 Base64Url 编码字符串的形式呈现。对于公共令牌,有效载荷以明文形式呈现,并经过 Base64Url 编码。
  • 页脚包含有效负载中未包含的可选元数据,例如受众(aud)或发行者(iss)。

本地 PASETO 与公共 PASETO

本地 PASETO 令牌经过加密,确保令牌内数据的机密性。它们适用于需要对敏感信息保密的场景,防止未经授权的第三方访问。这些令牌使用对称加密算法来保证有效载荷的机密性。只有拥有共享密钥的各方才能解密并访问令牌的内容。

公钥 PASETO 令牌经过签名。它们确保数据的完整性,但不保证数据的机密性。这些令牌是透明的,任何人都可以读取,但只有签名密钥的持有者才能验证。换句话说,任何篡改令牌的行为都会使签名失效。它们使用非对称加密算法(例如 ED25519)对令牌进行签名。公钥用于验证令牌,而私钥用于签名。它们适用于客户端需要读取有效载荷但又必须保持防篡改的场景。

PASETO面临的挑战

与应用广泛的 JWT 相比,PASETO 相对较新,采用率也较低。这意味着可用的库、工具和社区支持也相对较少。

熟悉 JWT 的开发者可能需要一些时间来理解和适应 PASETO 的原理和特性。虽然 PASETO 标准定义完善,但其生态系统缺乏 JWT 那样丰富的中间件和框架集成。

概括

本文详细介绍了多种身份验证技术。以下是主要学习要点的简要概述:

  • 身份验证是验证用户身份的过程,是应用程序安全的基础。它与授权不同,授权决定了已验证用户可以执行哪些操作。
  • Cookie 在客户端存储少量数据,并使用 HttpOnly、Secure 和 SameSite 等属性来控制安全性和行为。
  • 第一方 Cookie 由我们访问的域设置,对于身份验证至关重要;而第三方 Cookie 由外部域设置,并且越来越受到浏览器的限制。
  • 会话将身份验证数据存储在服务器端,并使用 cookie 仅保存会话 ID 引用,与在客户端存储敏感数据相比,安全性更高。
  • 会话存储选项包括内存存储(仅限开发)、数据库存储(单服务器)和分布式缓存(如 Redis,用于生产和分布式系统)。
  • 分布式会话策略包括粘性会话(简单但存在问题)、会话复制(开销高)和集中式会话存储(推荐用于微服务)。
  • JWT 是一种自包含的、无状态的令牌格式,由三部分组成(标头、有效负载、签名),无需服务器端会话存储。
  • 刷新令牌通过使用有效期短的访问令牌进行请求,并使用有效期长的刷新令牌来获取新的访问令牌,从而解决了 JWT 过期问题。
  • JWT 代币一旦发行就不容易撤销,需要额外的机制,例如代币黑名单或较短的过期时间,来管理安全性。
  • PASETO 通过强制执行强大的加密算法,并提供加密(本地)和签名(公共)令牌类型,改进了 JWT。
  • PASETO 通过移除算法协商并使用预设的安全默认值,消除了 JWT 算法混淆的漏洞。
  • 在 cookie/session(传统 Web 应用程序)、JWT(分布式系统和 API)或 PASETO(安全关键型应用程序)之间进行选择,取决于我们具体的可扩展性、安全性和架构要求。