认证词汇映射:API密钥、会话、JWT、OAuth2、OIDC、SSO
通过一个核心问题将 API 密钥、会话、JWT、OAuth2、OpenID Connect 以及 SSO 归类到一起,从而了解它们之间的关联:是谁在发起请求,或者他们能做些什么。
一名开发人员在一个项目中添加了“使用 Google 登录”功能,收到访问令牌后便认为任务已完成。这时同事提出了一个简单的问题:应用究竟是如何知道这是哪位用户的?通常真实的答案是它并不知道,因为所实现的其实是授权委托机制,而非登录功能。JWT、会话、OAuth2、OIDC 和 SSO 这些术语往往会被逐一解释,正因如此它们才容易混淆。本文将把它们全部放在同一张图表中,帮助你明确代码库中的每项技术实际上在解决什么问题,并为后续需求选择合适的工具。
每种认证机制背后的两个问题
这一领域的几乎所有技术都是为了解决两个问题之一。
- 身份认证要回答的是你是谁?它是一个验证身份的过程,无论该身份属于个人、后端服务还是设备。
- 权限授权要回答的是你被允许做什么?它在身份确认之后发挥作用,决定用户可以访问哪些资源以及能够执行哪些操作。
HTTP已经在其状态码中体现了这种区分。401 Unauthorized响应表示请求方未能证明自己的身份(尽管名称容易产生误解,但实际上涉及的是身份认证)。403 Forbidden响应则表示服务器清楚知道是谁在发起请求,但仍拒绝处理该请求。如果API因令牌缺失而返回403状态码,或因用户没有相应权限而返回401状态码,客户端就无法判断是应该提示用户登录还是显示“无访问权限”的消息。
一个具体的类比有助于理解。在办公室入口,保安会检查你的员工身份证;这就是身份认证。进入内部后,你的工作证可以打开某些门而无法打开其他门;这就是权限授权。前者用于确认身份,后者用于赋予权限,一个系统可以通过身份验证但失败于权限检查。如需更深入地了解每种验证在应用程序代码中的位置,请参阅身份认证与权限授权及其在代码中的位置。
在后续学习中请始终牢记这两个问题。一旦知道每种机制对应的是哪个问题,它们就会更容易理解。
API密钥:用于标识客户端,而非个人
API密钥是最简单的客户端身份验证方式。服务提供商会为你生成一个唯一的字符串,你需在每次请求中将其一同发送,服务器则会将该字符串与自身记录中的密钥进行比对。许多API会在标准请求头中接受该密钥作为身份凭证:
Authorization: Bearer your-api-key-here
其他服务提供商则使用自定义请求头,如X-API-Key;其原理相同。需要注意的是,密钥本身并不用于描述任何具体用户信息,它只是一个不可见的值,因此服务器必须通过查询该密钥来确定是哪个账户正在发起请求。除非手动设置过期时间,否则密钥没有内置的过期机制,而一旦密钥泄露,持有者将拥有与合法所有者相同的访问权限。因此,密钥的轮换、权限管控以及安全存储都由你负责。
另一种方式是HTTP基本认证,它会在请求头中发送经过Base64编码的用户名和密码。由于Base64仅属于编码方式而非加密手段,因此基本认证只有在通过TLS传输时才是安全的,对于非常简单的内部工具之外,其安全性难以得到保障。API密钥则是更为常见的轻量级解决方案,当你能控制连接的双方,或是服务需要按账户进行计费与流量限制时,使用API密钥尤为合适。
常见需要使用API密钥的场景包括:
- AI模型API
- 支付网关
- 天气及其他数据API
- 你自己开发的服务之间的后端调用
注意,上述列表中并未包含用户登录应用程序的情况。API密钥用于标识发起请求的应用程序或账户,而非屏幕前的操作人员。
会话:Cookie背后的服务器端状态
在 JWT 还未流行之前,会话就是保持用户登录状态的默认方式,至今它依然是服务器渲染型应用程序的优质选择。
其工作流程十分简单:用户提交凭证,服务器对其进行验证并在内存、Redis 或数据库等存储中创建会话记录,随后返回的响应中仅包含一个存储有会话 ID 的 Cookie。在后续的每次请求中,浏览器都会将此 Cookie 重新发送给服务器,服务器则通过该 ID 查找对应的会话及关联的用户信息。
这种设计具有状态性,而这也正是它的优势所在。服务器掌握着真实信息,因此要使用户登出或撤销已被入侵的会话,只需删除相应的记录即可。该 Cookie 本身除了一个随机标识符外,并不包含任何敏感信息。
当进行水平扩展时就会出现成本问题。如果有多台服务器处理流量,它们都需要访问同一个会话存储,否则在某台服务器上登录的用户在另一台服务器上就会显示为匿名用户。实际上,共享的 Redis 实例可以很好地解决这个问题,但它又增加了一个需要正确部署、监控和配置的组件,而配置错误的会话存储往往会在发布过程中最糟糕的时刻出现问题。
对于以下场景,会话机制依然非常适用:
- 传统的服务器端渲染网页应用
- 管理控制台
- 那些即时撤销功能比无状态性更重要的应用
JWT:自包含、已签名且可读取
JSON Web Token 是一种经过签名的 JSON 对象,其中包含用户 ID、角色以及过期时间等信息。它由三个通过点号连接的 base64url 编码的片段组成:
Header.Payload.Signature
头部字段用于指定签名算法,有效载荷中包含声明信息,而签名则使服务器能够确认在没有签名密钥的情况下没有任何人篡改过这两部分内容。由于声明信息存储在令牌内部,服务器无需查询数据库,只需验证签名并读取有效载荷即可对请求进行身份认证。正是这一特性使得JWT非常适合无状态和分布式系统。
已签名并不等同于保密
最常见的误解是认为JWT会隐藏其内容,其实并非如此。标准的JWT只是经过签名而非加密,任何持有它的人都可以通过一次base64解码操作来获取有效载荷。切勿将密码、不愿向用户展示的个人数据或内部机密信息放入JWT的声明中,以为这样就能保证其隐私性。当发生与JWT相关的问题时,往往正是这种错误的假设导致了问题。(根据JWE规范确实存在加密令牌,但那是另一种格式,通常并非教程中所说的“JWT”含义。)
访问令牌与刷新令牌
由于JWT在过期前一直有效,常见的做法是使用两种令牌搭配:
- 访问令牌:有效期较短,通常为15分钟到1小时,并随每个API请求一同发送。
应将刷新令牌存储在HttpOnly Cookie中,而非可能被注入脚本读取的localStorage中。短暂的访问令牌有效期能够限制泄露带来的危害,而刷新令牌则是实现令牌撤销逻辑的载体。
JWT非常适合用于API、移动客户端、单页应用以及分布式服务。它们并未让会话机制过时;只是用易于撤销的特性换取了无状态性,具体选择取决于您的架构。关于Node.js认证模型中会话机制与JWT的比较详细阐述了这种权衡关系。
OAuth 2.0:委托访问,而非登录
OAuth 2.0是最常被误归类的概念。它实际上是一种授权框架,而非身份验证协议。它的作用是让某个应用程序能够代表用户访问另一服务所持有的资源,而无需用户透露密码。
在常见的授权码流程中,您的应用程序会将用户重定向到服务提供商的同意页面,该页面会显示类似“是否允许此应用查看您的Google Drive文件?”的提示。如果用户同意,服务提供商会返回一个有效期短暂的授权码。您的后端随后用该代码换取访问令牌,再以此调用服务提供商的API。
该访问令牌代表了获取特定资源的权限。它并不说明用户的身份,OAuth2规范也未规定其格式,也不要求应用程序必须能够读取它。将访问令牌的出现视为用户已登录的证明,正是开头场景中典型的错误做法,这种做法曾导致过严重的安全漏洞,例如某个应用程序获得的令牌被另一个应用程序当作身份验证的依据。
OAuth2旨在回答如下问题:
- 该应用程序是否可以读取用户的Drive文件?
- 该应用程序是否可以访问用户的代码仓库?
它并非用于回答“这个用户是谁?”这样的问题。
OpenID Connect:构建在OAuth2之上的身份认证层
如果 OAuth2 能告知应用可以访问哪些资源,那么 OpenID Connect 则补充了关于用户身份的必要信息。OIDC 是构建在 OAuth2 之上的轻量级身份认证层,它复用了 OAuth2 的重定向和令牌交换机制。当您点击“使用 Google 登录”时,其背后运行的其实是 OIDC,而非单纯的 OAuth2。
用户完成身份验证后,OIDC 提供商会返回两种用途不同的令牌:
- ID 令牌,即描述用户的 JWT:它是一个稳定的唯一标识符,通常还包含姓名和邮箱等信息。
- 访问令牌,允许您的应用代表用户调用 API。
ID令牌用于验证用户身份,使其能够访问您的应用程序;而访问令牌则负责授权应用程序执行后续操作。在信任ID令牌所包含的信息之前,后端系统应先验证其签名、发行方、适用范围以及有效期,并且应以稳定的主体标识符而非可能变化的电子邮件地址来标记用户账户。从这个角度来看,仅靠OAuth2无法实现登录功能,需要OIDC来完成这一过程。
SSO:由协议实现的体验
单点登录经常被误认为是实现它的那些协议。实际上,SSO是一种用户体验模式——用户只需验证一次身份,之后便可在多个系统之间切换而无需再次登录。谷歌就是一个常见的例子:用户只需登录一次,Gmail、Drive、日历和YouTube就能识别出该用户。
在SSO背后起核心作用的是两种协议:
- SAML:基于XML且技术成熟,在企业门户、CRM控制台及内部工具等企业环境中占据主导地位。
- OpenID Connect:基于JSON和JWT,出现时间较晚,是Web应用和移动应用的常用选择。
SAML在实现与调试方面极为复杂,因此对于新系统而言,OIDC通常是更简便的选择。当需要与不支持OIDC的企业身份提供商集成时,SAML依然有其适用价值,而且这类系统至今仍在大量使用中。
当有人要求你“添加单点登录”功能时,首先要弄清楚他们的身份提供商支持哪种协议。这个答案将决定后续所需的库、配置及测试工作。
选择机制
综合以上信息,大致的决策指南如下:
- 您的服务由其他程序调用,而非人工操作:可使用有范围限制且会定期轮换的 API 密钥,或是在需要过期令牌时使用 OAuth2 客户端凭证。
- 服务器渲染的应用程序或具有独立登录功能的管理员面板:通过安全的
HttpOnlyCookie 来管理会话。 - 无状态 API、移动客户端或需验证同一用户的多个服务:使用带有刷新令牌的 JWT 访问令牌。
- 您的应用需要操作其他服务中的用户数据:采用 OAuth 2.0。
- 希望用户使用谷歌等现有账户登录:使用 OpenID Connect。
- 员工需要在多个内部工具或 SaaS 工具之间统一登录:在身份提供方要求的情况下,可使用 OIDC 实现 SSO,或使用 SAML。
这些选项可以组合使用。典型的产品可能会采用 OIDC 进行登录,然后生成自己的会话或 JWT,同时为第三方集成保留 OAuth2 访问令牌。
常见问题
在实践中,身份验证与授权有何区别?身份验证用于核实用户身份,失败时会返回 HTTP 401 错误;授权则用于检查权限,失败时返回 403 错误。二者是独立的步骤,拥有不同的错误代码,将它们混为一谈会导致严重的安全漏洞。
应该使用 JWT 还是会话?对于能够共享会话存储的服务器渲染应用,会话更为适用。而对于无状态 API、移动客户端和分布式系统,JWT 则更合适。决策时应根据架构需求及令牌撤销要求来定,而非仅看哪种选项听起来更现代。
OAuth2是用于身份验证还是授权? 是用于授权的。它允许应用程序在无需确认用户身份的情况下代表该用户访问资源。对于身份识别,则应使用OpenID Connect,它正是为这一目的对OAuth2进行扩展而设计的。
核心要点
- 将每种机制归结为一个问题:是谁在发起请求?还是他们能做什么?
- API密钥用于标识客户端应用程序;它们不包含用户身份信息,需要自行设置有效期并定期更换。
- 会话在服务器端保存状态,这便于撤销访问权限,但在大规模应用中需要共享存储机制。
- JWT是经过签名而非加密的;请将敏感信息置于载荷之外,同时不要将刷新令牌存放在
localStorage中。 - OAuth2提供委托访问权限;OIDC则增加了用于实际实现用户登录的ID令牌。
这一领域中大多数混淆,包括开头提到的“使用Google登录”概念的误解,都源于将两个问题混为一谈。应将身份管理与权限管理分开,此时选择哪种工具就变成了根据其设计目的来匹配相应问题。