← 返回蜂巢洞察

OAuth 2.0的工作原理:后端开发人员的实用指南

如果你问十位初级开发人员OAuth 2.0的原理,其中九个人会开始列举诸如“授权服务器”、“承载令牌”、“PKCE”和“隐式授权”之类的术语。他们还可能会画出一张包含六条来回箭头的序列图。 但当你问他们为什么会有某个特定的HTTP请求存在,或者如果去掉这个请求会发生什么问题时,他们往往无法给出合理的解释。 在我看来,这是因为人们通常是以逆向的方式来学习OAuth的。大多数教程都是先介绍定义和序列图,然后再解释为什么该协议会被设计成这样的结构。 在本书中,我们将改变这种教学方式。我们会从实际工程问题出发,逐步构建OAuth 2.0的概念体系,从而自然地理解这一协议的运作原理。当我们开始使用Spr

如果你问十位初级开发人员OAuth 2.0的原理,其中九个人会开始列举诸如“授权服务器”、“承载令牌”、“PKCE”和“隐式授权”之类的术语。他们还可能会画出一张包含六条来回箭头的序列图。

但当你问他们为什么会有某个特定的HTTP请求存在,或者如果去掉这个请求会发生什么问题时,他们往往无法给出合理的解释。

在我看来,这是因为人们通常是以逆向的方式来学习OAuth的。大多数教程都是先介绍定义和序列图,然后再解释为什么该协议会被设计成这样的结构。

在本书中,我们将改变这种教学方式。我们会从实际工程问题出发,逐步构建OAuth 2.0的概念体系,从而自然地理解这一协议的运作原理。当我们开始使用Spring Boot编写代码时,每一个参数、重定向路径以及令牌的含义都会变得一清二楚。

我们将涵盖以下内容:

OAuth出现之前的问题

假设我们正在开发一个名为TravelBuddy的Spring Boot应用,这个应用可以帮助用户规划旅行行程。

TravelBuddy具有自动检测日程冲突的功能,并能将旅行计划直接添加到用户的Google日历中。

为了实现这一功能,TravelBuddy需要访问Google日历的API。具体来说,它需要读取现有的事件信息,同时也需要代表用户Alice来创建新的事件记录。

那么在2005年OAuth技术还不存在的时候,我们该如何解决这个问题呢?

密码共享的反模式

如果没有像OAuth这样的协议,TravelBuddy就不得不向Alice询问她的Google用户名和密码。

这是一个线性流程图,显示Alice直接将她的完整账户凭证发送给TravelBuddy,而TravelBuddy再把这些凭证转发给Google Calendar API。这种模式迫使用户将全部账户控制权交给第三方应用程序。

Alice会直接在TravelBuddy的用户界面中输入她的Google密码。TravelBuddy会将这个密码存储在自己的数据库中,每当需要获取或创建日历事件时,就会使用这些凭证来登录Google。

这种做法虽然可行,但却会带来严重的安全问题和运营问题:

  1. 过度授权的问题: TravelBuddy只需要管理日历事件而已,但由于它掌握了Alice的Google密码,因此它还可以查看她的Gmail邮件、浏览她的Google Drive文件、删除她的照片,甚至更改她的账户密码。根本不可能给TravelBudy只赋予有限的访问权限。

  2. 无法实现细粒度的权限撤销:如果Alice想阻止TravelBuddy访问她的日历,她唯一的办法就是更改自己的Google密码。但这样做的话,她之前授权给其他所有应用程序的权限也会随之被取消。

  3. TravelBuddy需要承担存储密码的责任:现在,TravelBudy正在为成千上万的Google账户存储明文或可解密的密码。如果TravelBuddy的系统发生SQL注入攻击或者数据库泄露,那么用户们在Google上的所有数字信息都会面临安全风险。

  4. 让用户习惯于在第三方应用程序中输入自己的主要Google账号凭证,这种做法会让他们养成非常糟糕的安全习惯。

我们需要一种方法,让Alice能够在从不向TravelBuddy提供其Google密码的情况下,允许它执行某些特定的操作。

这种功能被称为委托授权,而OAuth 2.0恰恰提供了这样的机制。

OAuth 2.0到底是什么(以及它不是什么)

OAuth 2.0是一种用于委托授权的开放标准。

它提供了一个框架,使用户能够在不共享自己凭证的情况下,允许第三方应用程序访问自己在其他服务上的资源。

在继续讨论之前,我们必须先纠正Web开发领域中最常见的一个误解。

认证与授权

开发者们经常将这两个术语互换使用,但实际上它们代表着两个根本不同的概念:

  • 认证(AuthN): 你是谁?(身份验证)

  • 授权(AuthZ): 你被允许做什么?(权限控制)

一张流程图,说明了认证在授权之前的重要性。第一个环节用于确认用户身份(“你是Alice”),这一信息会被用于第二个环节以确定用户的权限范围(“Alice可以读取/写入事件记录,但无法删除日历”)。

OAuth 2.0严格来说只是一个授权框架。它并不规定如何验证用户的身份、如何生成身份认证信息,也不涉及用户账户的存储问题。它的唯一功能就是颁发权限令牌,这样某个服务就可以代表用户与另一个服务进行交互。

当你在网站上点击“使用Google登录”时,实际上使用的是一种建立在OAuth基础之上的技术——OpenID Connect(OIDC),我们稍后会详细介绍这一点。但 OAuth 2.0的核心功能确实仅仅是授权。

应用注册:凭证的来源

在TravelBuddy能够启动OAuth流程之前,我们必须先在Google Cloud Console中完成注册手续。

在注册过程中,Google会要求TravelBuddy提供以下两项关键信息:

  1. 应用名称与标志:这些信息会显示在用户的同意页面上。

  2. 重定向URL:也就是允许Google发送授权码的具体回调地址(例如:https://travelbuddy.com/login/oauth2/code/google)。

完成注册后,Google会向TravelBuddy提供两项凭证:

  • client_id:这是一个公共标识符,类似于用户名,用于识别TravelBuddy。将其嵌入到公开链接或前端代码中是安全的。

  • client_secret:这是一项保密密钥,类似于密码,TravelBuddy的后端服务器在交换授权码与令牌时需要使用它来进行身份验证。

OAuth 2.0中的四种角色

OAuth 2.0定义了四种角色。让我们将这些概念直接应用到TravelBuddy的例子中,这样这些术语就不再只是抽象的概念了。

一张图表,展示了四个实体之间的关系:Alice(资源所有者)授权TravelBuddy(客户端)访问数据。Alice先通过Google的授权服务器进行身份验证,然后该服务器会颁发令牌给TravelBuddy,TravelBuddy再使用这个令牌向Google Calendar(资源服务器)请求数据,而Google Calendar也会通过授权服务器来验证这个令牌的有效性。
  • 资源所有者:拥有数据的用户。在我们的例子中,这个人就是Alice,她拥有自己的Google日历。

  • 客户端:试图访问用户数据的第三方应用程序。在我们的例子中,这个客户端就是TravelBuddy(我们的Spring Boot后端服务)。之所以称为“客户端”,是因为它是以用户的身份与API进行交互的。

  • 授权服务器:负责验证用户身份、获取用户的同意,并颁发访问令牌的服务器。在我们的例子中,这个服务器就是Google的OAuth服务器(地址为accounts.google.com)。

  • 资源服务器:存储受保护用户数据的服务器。在我们的例子中,这个服务器就是Google日历API(地址为www.googleapis.com/calendar)。

请注意,谷歌的职责被划分成了两个不同的角色:授权服务器(负责发放令牌)和资源服务器(用于托管API)。在大型组织中,这些通常是由不同团队维护的独立服务。

访问令牌与权限范围

爱丽丝并没有将密码提供给TravelBuddy,而是同意发放一个访问令牌

访问令牌是一串字符,它的作用类似于临时门禁卡。当TravelBuddy向Google Calendar发起HTTP请求时,它会在请求头中包含这个令牌。

访问令牌具有密码所不具备的三个关键特性:

  1. 权限范围有限:它只能用于特定的操作权限。

  2. 有效期有限:它会在一段时间后自动失效(通常是几分钟或几小时)。

  3. 可撤销:爱丽丝或谷歌可以在任何时候撤销这个令牌,而不会影响爱丽丝的账户密码。

什么是权限范围?

权限范围是一段字符串,用于明确说明所请求的具体操作权限。TravelBuddy并不会要求“访问Google账户”,而是会具体指定所需的权限范围。

当TravelBuddy将爱丽丝重定向到Google时,它会明确指出所请求的权限范围:

当TravelBuddy将爱丽丝重定向到Google时,它会明确说明所请求的权限范围。谷歌会向爱丽丝展示这些具体的权限:

“TravelBuddy需要获得查看和编辑您Google日历事件的权限。”

如果爱丽丝同意,谷歌发放的访问令牌将会严格限定在这些被请求的权限范围内。如果TravelBuddy试图使用同一个令牌来读取爱丽丝的电子邮件,谷歌的资源服务器会以403 Forbidden状态码拒绝该请求。

授权代码流程

现在我们了解了这些角色和令牌的概念,那么TravelBuddy究竟是如何获得访问令牌的呢?

对于服务器端应用程序来说,最标准且最安全的流程就是授权代码流程

以下是具体的操作步骤。在查看图表之后,我们会详细解释每一个步骤。

一个包含九个步骤的流程图,详细说明了授权过程。用户首先发起同步请求,然后被重定向到Google进行登录并确认许可,随后通过浏览器重定向收到临时授权代码。TravelBuddy的后端会使用这个代码及其客户端密钥直接从Google获取访问令牌,之后再使用该令牌调用日历API。

让我们一步步来分析这个过程。

步骤1:用户发起操作

Alice正在使用TravelBuddy的用户界面,然后点击了“连接Google日历”按钮。

步骤2:TravelBuddy生成重定向链接

TravelBuddy的后端并不会要求用户输入凭证信息。相反,它会生成一个指向Google授权服务器的URL,并指示Alice的浏览器跳转到该地址。

这个URL的具体格式如下:

GET https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=TRAVELBUDDY_CLIENT_ID&redirect_uri=https://travelbuddy.com/login/oauth2/code/google&scope=https://www.googleapis.com/auth/calendar.events&state=xyz123

让我们来分析一下每个参数的作用:

  • response_type=code:这个参数告诉Google我们使用的是授权码流程。

  • client_id:这是TravelBuddy在注册为开发应用时,由Google分配给它的公共标识符。

  • redirect_uri:当Alice完成授权流程后,Google应该将她重定向到这个URL地址。

  • scope:TravelBuddy请求获取的权限范围。

  • state:TravelBuddy生成的一个随机字符串,用于防止跨站请求伪造攻击。

步骤3:Alice进行身份验证并同意授权

Alice的浏览器会跳转到Google的域名页面(accounts.google.com)。

Google会检查Alice是否已经登录。如果还没有登录,系统会要求她进行登录。TravelBuddy根本看不到这一交互过程。

一旦Alice完成了身份验证,Google就会显示一个授权页面,上面会列出TravelBudy的名称以及它所请求的权限范围。

步骤4与步骤5:Google生成授权码

Alice点击“批准”按钮后,Google的授权服务器会将她的浏览器重定向回TravelBudy之前注册的redirect_uri地址,并在请求字符串中附加一个临时有效的授权码以及state参数:

GET https://travelbuddy.com/login/oauth2/code/google?code=4/0AX4XfWh...&state=xyz123

TravelBuddy的后端会验证返回的state值是否与它之前发送的值一致。如果一致,就会获取到这个授权码

步骤6与步骤7:TravelBuddy用授权码换取令牌

接下来,TravelBudy的后端服务器会直接向Google的令牌接口发送一个POST请求(地址为https://oauth2.googleapis.com/token):

POST /token HTTP/1.1
Host: oauth2.googleapis.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=4/0AX4XfWh...&redirect_uri=https://travelbuddy.com/login/oauth2/code/google&client_id=TRAVELBUDDY_CLIENT_ID&client_secret=TRAVELBUDDY_CLIENT_SECRET

Google会验证这个授权代码以及TravelBuddy的client_secret。如果验证通过,Google会返回一个包含访问令牌的JSON响应:

{
  "access_token": "ya29.a0ARrdaM...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "1//04rG...",
  "scope": "https://www.googleapis.com/auth/calendar.events"
}

步骤8与步骤9:调用API

TravelBuddy会将这个访问令牌安全地存储起来,然后使用它来代表Alice调用Google Calendar API:

GET /calendar/v3/users/me/calendarList HTTP/1.1
Host: www.googleapis.com
Authorization: Bearer ya29.a0ARrdaM...

Google Calendar会接收这个请求,提取出其中的访问令牌,然后通过自身的认证系统来验证该令牌是否有效、其使用范围是否正确,最终会返回Alice的日历数据。

为什么需要这种两步验证机制

在这个阶段,初级开发人员几乎总会提出这样一个很好的问题:

“为什么要有步骤4和步骤6?为什么Google不在步骤4中将访问令牌直接返回给浏览器呢?”

为什么要先向浏览器返回一个临时的authorization_code,然后再立即进行后端调用来换取真正的access_token呢?

答案归根结底在于前端通道与后端通道的安全性差异

  • 前端通道(浏览器): 浏览器是一个不可信任且容易受到攻击的环境。在请求过程中,路由地址会经过浏览器的历史记录、系统日志、引用头信息以及浏览器扩展程序等环节。如果Google直接在浏览器的URL中返回访问令牌,那么这个高权限的令牌就很有可能被恶意插件窃取或拦截。

  • 后端通道(服务器之间的通信): TravelBuddy的后端服务器与Google的认证服务器之间是通过加密的HTTPS协议进行直接通信的,这种通信完全绕过了浏览器。

一种用于说明两种网络通信机制的结构图。上方的部分表示前端通道,其中浏览器通过URL重定向来传递授权代码;下方的部分表示后端通道,TravelBuddy的服务器会通过加密的HTTPS协议直接交换授权代码和客户端密钥以获取访问令牌。

授权代码实际上是一种临时性的、一次性使用的凭证(通常在60秒内失效)。即使攻击者从浏览器的历史记录中窃取了这个授权代码,他们也无法将其兑换成访问令牌,因为他们并不拥有TravelBuddy的client_secret

令牌过期与刷新令牌

访问令牌被设计成具有较短的生命周期,通常在一小时后会失效(expires_in: 3600)。

为什么这样设计呢?因为如果访问令牌泄露,攻击者可利用的时间仅限于该令牌在过期前剩余的这段时间。

但如果要求用户每小时都重新进行身份验证并点击“批准”,那么用户体验将会非常糟糕。毕竟,在用户睡觉时,TravelBuddy仍然需要在后台同步日历信息。

为了解决这个问题,OAuth 2.0引入了刷新令牌

一张序列图,展示了错误恢复的过程:TravelBuddy使用过期的令牌尝试调用API,收到了401响应;随后它使用刷新令牌向认证服务器发送POST请求,获得了新的访问令牌,并成功重新尝试了原来的API请求。

刷新令牌的工作原理

根据提供者的配置设置(例如,对于Google来说,需要传递access_type=offlineprompt=consent参数),在初次进行代码交换时,Google会同时返回一个访问令牌和一个有效期较长的刷新令牌

之后,TravelBuddy会将这个刷新令牌加密后安全地存储在其数据库中。

访问令牌失效时,TravelBuddy会直接在后台向Google的令牌接口发送请求,同时提供刷新令牌客户端密钥

最终,Google会验证这个刷新令牌,并颁发一个新的访问令牌,而整个过程完全不需要用户的参与。

用于获取新的访问令牌

非常短(15分钟到1小时)

有效期较长(数天、数月,或直到被撤销为止)

发送目的地

每次向资源服务器发起API请求时都会一同发送

仅发送到认证服务器的令牌接口

存储安全性

可以暂时存储在服务器内存中

必须加密后保存在安全的存储空间中

功能

访问令牌

刷新令牌

主要用途

用于访问受保护的API

有效期

PKCE:保护公共客户端

我们刚才讨论的认证代码流程依赖于TravelBuddy对其客户端密钥的保密性。正因为如此,TravelBudy被归类为机密客户端。它运行在服务器上,开发者可以在该服务器上安全地存储各种环境变量和敏感信息。

但如果TravelBuddy是一个单页应用(在浏览器中直接运行的React/Vue)或一个原生移动应用(iOS/Android)呢?

这些都属于公共客户端。任何人都可以打开浏览器的开发者工具,或者反编译Android的.apk文件来提取其中嵌入的client_secret

如果没有这个密钥,公共客户端就无法安全地使用授权码流程。如果移动设备上的恶意应用截获了授权码,它就可以用这个代码去换取令牌,因为没有client_secret来阻止这种行为。

为了解决这个问题,OAuth 2.0引入了PKCE(用于代码交换的证明密钥)。

一张示意图,说明了PKCE的工作原理。客户端生成一个名为<code>code_verifier</code>的加密随机字符串,并将其哈希处理成<code>code_challenge</code>。在授权过程中,客户端会发送这个<code>codechallenge</code>;而在将授权码换取令牌时,客户端会提供原始的<code>code_verifier</code>。授权服务器会对这个<code>code_verifier</code>进行哈希处理,并与之前保存的<code>code_challenge</code>进行比对,从而验证客户的身份,而无需使用<code>client_secret</code>。

PKCE的工作原理

在开始授权流程之前,客户端会生成一个加密的随机字符串,称为code_verifier。然后客户端会将这个字符串进行哈希处理(通常使用SHA-256算法),从而得到codechallenge

在授权流程的第二步中,客户端会将code_challenge以及其使用的哈希算法codeChallengeMethod=S256发送给授权服务器。授权服务器会保存这个codechallenge,然后像往常一样返回授权码。

在第六步中,当客户端将授权码换取令牌时,它会提供原始的、未经哈希处理的code_verifier

授权服务器会使用SHA-256算法对客户端提供的code_verifier进行哈希处理,然后检查它是否与之前保存的codechallenge相匹配。如果匹配成功,那就说明请求令牌的应用程序就是最初发起请求的那个应用程序。

注意:现代的OAuth安全指南建议所有应用都使用PKCE,包括像Spring Boot这样的保密后端应用。

state与PKCE:防止不同类型的攻击

开发者们经常会将state和PKCE混淆,因为它们都在OAuth流程中涉及随机字符串的传递。但实际上,它们所起的作用是用来防范完全不同的攻击手段:

区别所在

state参数

PKCE(code_verifier)

主要面临的威胁

登录CSRF攻击:攻击者会诱使受害者使用攻击者自己的授权码来完成OAuth流程。

代码拦截攻击:攻击者会窃取受害者的授权码,并用它来换取令牌。

它们的防护机制

将授权回调与用户特定的浏览器会话关联起来。

确保交换代码的一方确实就是最初发起请求的那方。

验证方式

客户端应用程序后端在回调时进行验证。

授权服务器/token接口处进行验证。

OAuth 2.0与OpenID Connect(OIDC)及JWTs

早前我曾强调过,OAuth 2.0纯粹是用于授权(确定用户是否具有访问某项资源的权限),而非身份验证(确认用户的真实身份)。

然而,你使用的几乎每一个应用都设有“使用Google登录”的按钮。这种功能究竟是如何实现的呢?

了解OpenID Connect(OIDC)

OpenID Connect实际上是在OAuth 2.0的基础上构建的一种身份验证机制。

虽然OAuth的核心功能是颁发用于API访问的access_token,但当请求包含openid权限范围时,授权服务器会同时颁发一个ID Token以及access_token:

一张架构图,展示了OpenID Connect作为外层身份验证机制如何覆盖OAuth 2.0。OAuth 2.0负责处理API访问所需的access_token,而OIDC则添加了ID Token来传递用户的身份信息。

当TravelBuddy同时请求openid权限范围和日历访问权限时:

scope=openid profile email https://www.googleapis.com/auth/calendar.events

Google的令牌服务端会同时返回access_tokenid_token

  • Access Token:专用于资源服务器(例如Google日历)。TravelBuddy无需解析其内容,只需将其作为请求头信息传递即可。

  • ID Token:专门为TravelBuddy设计。其中包含了关于用户身份的加密信息,例如用户的Google用户名、全名、电子邮件地址以及个人资料图片的URL。

什么是JWT?

ID Token几乎总是以JWT(JSON Web Token)的形式存在。

JWT是一种结构紧凑、适合在URL中传递的数据格式,它由三部分组成:HeaderPayloadSignature,这三部分之间用点号分隔。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

解码其中的中间部分Payload后,会得到普通的JSON格式数据:

{
  "sub": "google-user-id-98765",
  "iss": "https://accounts.google.com",
  "aud": "TRAVELBUDDY_CLIENT_ID",
  "email": "alice@gmail.com",
  "exp": 1711900000
}

需要注意的是,Base64URL编码并不属于加密技术。任何拥有JWT的人都可以读取其中的内容。但由于该令牌使用了Google的私钥进行签名,因此TravelBuddy的后端可以在本地验证这些签名信息,而无需每次请求都与Google服务器进行交互来确认用户的身份。

Access Token一定是JWT吗?

不。 OAuth 2.0并不强制规定访问令牌必须采用任何特定的格式。

访问令牌可以是以下两种类型之一:

  1. 透明令牌:完全随机的字符串(例如 ya29.a0ARrdaM...)。客户端和资源服务器需要通过向授权服务器查询来了解该令牌的含义。

  2. 结构化令牌(如 JWT):其中包含嵌入式信息,因此资源服务器可以自行对其进行验证。

什么是 OAuth 2.1?

如果你遵循现代的安全规范,那么你一定听说过 OAuth 2.1

OAuth 2.1并不是对 OAuth 2.0的彻底改造,而是一个将多年积累的安全最佳实践整合到一起的规范草案:

  1. 强制要求使用 PKCE:对于所有授权码流程来说,PKCE都是必需的——包括像 Spring Boot 这样的服务器端应用程序。

  2. 淘汰旧有的授权方式:“隐式授权”(会在浏览器 URL 中直接返回令牌)以及“资源所有者密码认证授权”方式已被完全弃用。

  3. 严格匹配重定向 URI:为了防止被利用进行恶意重定向攻击,禁止在重定向 URI中使用通配符。

需要避免的生产环境安全隐患

在生产环境中实现 OAuth 集成时,必须格外注意细节。以下是后端工程师常会犯的五种安全错误,以及相应的防范措施:

1. 将令牌存储在浏览器的 localStorage

如果你正在开发单页应用程序,并且这些应用会接收访问令牌或刷新令牌,那么绝对不要将这些令牌保存在 localStoragesessionStorage 中。

任何在你的页面上运行的脚本,包括第三方分析工具、聊天插件,或是受到攻击的 npm 依赖项,都可能通过跨站脚本攻击(XSS)读取 localStorage 中的内容。

解决方法:应该将令牌存储在后端管理的、仅能通过 HTTP 进行访问且经过加密处理的 SameSite cookie 中;或者采用后端为前端服务的架构,这样令牌就根本不会到达浏览器。

2. 泄露客户端密钥

这听起来似乎很显而易见,但实际上 client_secret 这种字符串却经常会被发布到公开的 GitHub 仓库中。请记住:任何被包含在 Android/iOS 应用程序、React 单页应用程序或前端代码中的敏感信息都是公开可见的。

解决方法:务必将客户端密钥保存在服务器端的环境变量中;对于那些无法进行安全保护的公共客户端,可以使用 PKCE 这种机制来处理令牌的传输问题。

3. 请求不必要的权限范围

如果你只需要读取某些数据,却请求了完整的账户访问权限,这会让用户产生怀疑;而且如果令牌发生泄漏,也会增加你的风险。

修复措施:遵循最小权限原则。仅请求应用程序当前所需的特定权限范围。如果TravelBuddy后续添加了分析电子邮件的功能,那么应在用户启用该功能时动态地请求Gmail相关的权限。

4. 假设OAuth令牌能够证明身份

仅仅因为一个应用程序从API获得了access_token,并不意味着它就可以将这个令牌视为登录用户的凭证。

如果攻击者使用了从其他应用程序获取的有效访问令牌(这种攻击方式被称为“混淆代理攻击”),而你的系统只检查了令牌的有效性,而没有验证该令牌是发给谁的aud/目标用户群体,那么你的系统很可能会接受这个令牌。

修复措施:在验证用户身份时,应使用OpenID Connect,并验证id_token中包含的audiss等信息。

5. 跳过statePKCE验证

如果你在构建OAuth流程时没有检查state参数,那么你的应用程序就容易受到跨站请求伪造攻击。攻击者可以诱使用户的浏览器使用自己的授权码来完成 OAuth流程,从而将受害者的会话信息关联到攻击者的账户上。

修复措施:始终生成一个加密强度高、无法被猜测的state参数,并将其与用户的会话信息绑定在一起;或者依赖像Spring Security这样的安全框架来自动执行这一操作。

最后的思考

如果仅仅从规范、RFC文档和专业术语的角度来看,OAuth 2.0确实显得相当复杂。

但一旦抛开这些专业术语,就会发现OAuth实际上解决了一个核心问题:它允许用户在不泄露密码的情况下,允许某些应用程序访问自己的数据。

该协议中的每一个组成部分都是为了安全地实现这一核心目标而存在的:

  • 权限范围用于限制应用程序可获得的权限。

  • 访问令牌提供了临时且可撤销的访问权限。

  • 授权码可以防止令牌被用于不安全的浏览器URL中。

  • 刷新令牌能够在不影响用户体验的情况下维持长期访问权限。

  • PKCE为那些无法保管机密信息的公共应用程序提供了保护机制。

  • OpenID Connect则为身份验证过程增加了一层标准化机制。

下次在Spring Boot中集成OAuth提供者,或者在生产环境中调试令牌相关问题时,不要一味地记忆各种图表。而是要关注HTTP请求本身,弄清楚应用程序正在跨越哪些权限边界,这样相关的设计原理就会立刻变得清晰起来。

术语汇总
  • 授权码:一种有效期较短、仅能使用一次的令牌,通过浏览器重定向返回,在服务器端用于换取访问令牌。

  • 访问令牌:一种临时性的访问凭证,用于在HTTP请求头中指定对受保护资源的访问权限。

  • 刷新令牌:一种长期有效的凭证,专门用于从令牌服务端获取新的访问令牌。

  • 权限范围:一个字符串,用于明确客户端请求的具体访问权限。

  • PKCE:一种加密技术,用于确保令牌交换过程仅发生在发起请求的客户端身上。

  • OpenID Connect (OIDC):建立在OAuth 2.0基础上的身份验证框架,能够生成包含用户信息在内的id_token

  • JWT:一种紧凑的、经过数字签名的JSON格式,常用于表示身份令牌。

相关文章

技术实践

在调试BLE多点连接断开问题时,在阿里巴巴的网站上发现了音频指纹识别技术被使用的痕迹。

最近有研究发现,AliExpress利用Web Audio API,通过无声音频流来识别用户的设备类型。这种技术是通过分析各种硬件设备特有的音频处理方式来区分不同用户设备的。那些注重保护用户隐私的浏览器已经开发出了相应的应对措施,但这也暴露出现行网页标准在音频处理机制及用户隐私保护方面存在的安全漏洞。 作者:Olimpiu Pop

阅读全文
技术实践

如何测试对话式人工智能:面向质量保证工程师的实用指南

当我刚开始学习对话式人工智能测试时,有一个问题一直困扰着我: 预期的结果到底在哪里? 由于我之前从事过传统的软件测试工作,所以我习惯于一种固定的流程。 需求文档会告诉我们系统应该完成什么功能。我们创建测试用例,提供输入数据,定义预期结果,执行测试,然后将实际结果与预期结果进行对比。 举个例子: 测试项 输入内容 预期结果 有效登录 正确的用户名和密码 用户成功登录 无效登录 错误的密码 显示错误提示信息 API请求 有效的请求数据 收到HTTP 200响应及预期内容 但当我开始接触对话式人工智能时,我发现同样的测试方法不再适用了。 如果我向AI系统询问“如何重置密码?”,它可能会回答:“您可以

阅读全文
技术实践

如何利用WCAG 2.2标准打造更加易于访问的网站

一个网站可能看起来很专业,使用鼠标操作时也能运行得非常顺畅,但对某些用户来说,使用它仍然会遇到困难。 有时,某个表单会仅通过颜色来提示用户出现了问题;固定的页眉可能会完全覆盖当前处于键盘焦点位置的元素;登录表单可能会阻止用户从密码管理工具中复制密码并粘贴到表单中;而某个自定义按钮,用鼠标点击时可以正常使用,但当人们使用键盘操作时却毫无反应。 这些其实都是开发过程中做出的决策,并非只有在进行无障碍性审核时才会出现的问题。 《Web内容无障碍性指南》为识别和消除这些障碍提供了统一的标准。WCAG 2.2是最新的WCAG 2建议标准,世界万维网联盟也建议开发人员和各类组织在可能的情况下使用WCAG

阅读全文
技术实践

JEP 540的建议是将该功能应用于JDK 28版本,并提供简单的JSON接口供使用。

JEP 540——这个简单JSON API项目,目前已经进入了JDK 28版本的开发阶段。该API提供了一种无需依赖任何外部库即可解析和生成JSON文档的功能。它专注于实现核心功能,同时提供了不可变的值结构;这种API允许用户进行简单的遍历和数据转换操作,并且严格遵守相应的语法规范。在测试阶段收集到的用户反馈将会对它的后续发展产生重要影响。 作者:A N M Bazlur Rahman

阅读全文