# 前言

本教程将结合 Gemini 2.5 Pro 来为各位进行讲解

OAuth 2.0 是一个行业标准的授权框架,它允许第三方应用程序在不获取用户凭证(如用户名和密码)的情况下,有限度地访问用户在某一网站上存储的私有资源。

OAuth 2.0 中的四个核心角色:

  • 资源所有者 (Resource Owner):通常是终端用户,拥有受保护资源的最终控制权。

  • 客户端 (Client):希望访问资源所有者受保护资源的第三方应用程序。

  • 授权服务器 (Authorization Server):负责验证资源所有者的身份,并在获得资源所有者授权后,向客户端颁发访问令牌(Access Token)。

  • 资源服务器 (Resource Server):存储受保护资源,并接受和验证访问令牌,以允许客户端访问资源。

# 授权码模式 (Authorization Code Grant)

这是功能最完善、安全性最高的授权模式,因为需要保持 client_secret 非公开,推荐用于传统的 Web 应用(即拥有后端的应用)。

下文的 客户端 包括客户端的前端与后端,资源所有者一般为用户

sequenceDiagram
    participant ResourceOwner as 资源所有者
    participant Client as 客户端
    participant AuthServer as 授权服务器
    participant ResourceServer as 资源服务器
    ResourceOwner->>Client: 1. 访问客户端
    Client-->>ResourceOwner: 2. 返回授权链接
    ResourceOwner->>AuthServer: 3. 通过链接访问授权页面
    AuthServer-->>ResourceOwner: 4. 展示授权界面
    ResourceOwner->>AuthServer: 5. 确认授权
    AuthServer->>Client: 6. 重定向回调(携带code)
    Client->>AuthServer: 7. 发送code + client_secret请求token
    AuthServer-->>Client: 8. 返回access_token
    Client->>ResourceServer: 9. 携带access_token请求资源
    ResourceServer-->>Client: 10. 返回受保护资源
    note left of ResourceOwner: 用户操作流
    note right of AuthServer: 安全校验:
    note right of AuthServer: - 验证code有效性<br/>- 验证client_secret<br/>- 检查重定向URI

# 发起授权请求

当用户使用客户端时,客户端需要发起授权请求。

该请求实际由用户发起,客户端负责构建一个包含 client_idredirect_uriresponse_typescope 等参数的 URL 授权请求链接:

Base URL :授权服务器的授权 Endpoint,例如 https://xxx.com/api/authorize
response_typecode
client_id :客户端 ID
redirect_uri :回调 URL
scope :资源(权限)范围,例如 openidemailprofile
state :随机字符,可选(推荐),用于防止跨站请求伪造(CSRF)攻击

例如:

1
GET /authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=CALLBACK_URL&scope=SCOPE

# 确认授权

用户定向到授权链接,如果此时用户未登录,则会要求用户进行登录

登录完成后,授权服务器会展示本次授权请求相关信息,例如客户端的详情,请求的资源内容等

如果用户确认授权,则重定向至 redirect_uri(上文示例中的 CALLBACK_URL 内容)并在 URL 参数中携带本次确认授权的 AUTHORIZATION_CODE

Tips:正常情况下,授权服务器会对 redirect_uri 的内容进行校验,确保当前授权链接中的 redirect_uri 内容与客户端在授权服务器注册时预留的 redirect_uri 内容一致,否则重定向至钓鱼链接会导致本次确认授权的 AUTHORIZATION_CODE 泄露

# 客户端请求 access_token

客户端通过 redirect_uri 拿到本次授权的 AUTHORIZATION_CODE 后,使用 AUTHORIZATION_CODE 和 client_secret 向授权服务器请求 access_token

如果客户端启用了 state ,还会校验当前回调链接中的 state 值是否和当初授权链接中的 state 值一致,确保 AUTHORIZATION_CODE 仅用于本次授权请求会话,防止跨站请求伪造(CSRF)攻击

Base URL:授权服务器的令牌 Endpoint,例如 https://xxx.com/api/token

客户端向 Base URL 发送 POST 请求,请求体 Content-Typeapplication/x-www-form-urlencoded ,请求体内容如下:

grant_typeauthorization_code
code :上文的 AUTHORIZATION_CODE
redirect_uri :回调 URL
client_id :客户端 ID
client_secret :客户端 Secret

client_secret 不可直接暴露给客户端前端,必须由安全的客户端后端使用,否则任何人(包括攻击者)只需要拿到 AUTHORIZATION_CODE 即可向授权服务器发起 access_token 请求

# 客户端请求资源

当授权服务器返回 access_token 后,使用 access_token 向资源服务器请求所需要的资源

资源服务器对 access_token 进行一系列校验,确认身份后返回客户端所需要的资源

# 客户端凭证模式 (Client Credentials Grant)

该模式适用于没有特定用户参与的场景,即客户端以自己的名义访问资源(例如,机器对机器的通信)。

因为没有用户参与,因此这里客户端可以直接向令牌 Endpoint 请求 access_token

该模式需要具体参考授权服务器的文档,例如 authentik 身份识别基于服务账户,身份验证基于应用密码令牌(client_secret),因此还需要传递服务账户的 username 和 password,这里仅示例通用情况。

# 客户端请求 access_token

Base URL:授权服务器的令牌 Endpoint,例如 https://xxx.com/api/token

客户端向 Base URL 发送 POST 请求,请求体 Content-Typeapplication/x-www-form-urlencoded ,请求体内容如下:

grant_typeclient_credentials
scope :权限范围,具体参考授权服务器文档

请求头:

AuthorizationBasic ${将 client_id:client_secret 进行 Base64 编码后的字符串}

当然也可以将客户端凭证放在请求体中:

grant_typeclient_credentials
scope :权限范围,具体参考授权服务器文档
client_id :客户端 ID
client_secret :客户端 Secret

# 客户端请求资源

当授权服务器返回 access_token 后,客户端使用 access_token 向资源服务器请求所需要的资源

资源服务器对 access_token 进行一系列校验,确认身份后返回客户端所需要的资源

# 授权码 + PKCE (Proof Key for Code Exchange) 模式

PKCE 是对授权码模式的增强,现在被认为是所有类型客户端(包括原生应用、单页应用和传统 Web 应用)的最佳实践。

核心思想:在授权请求时,客户端生成一个随机的验证器( code_verifier ),并将其哈希值( code_challenge )发送给授权服务器。当客户端用授权码交换访问令牌时,必须同时提供原始的 code_verifier 。授权服务器会验证 code_verifiercode_challenge 是否匹配,从而确保即使授权码被截获,攻击者也无法在没有 code_verifier 的情况下冒用。

sequenceDiagram
    participant C as 客户端 (App)
    participant U as 资源所有者
    participant AS as 授权服务器
    participant RS as 资源服务器

    Note over C: 1. 生成 code_verifier, <br/>   计算 code_challenge
    C->>U: 2. 发起授权请求 (携带 code_challenge)
    U->>AS: 用户浏览器重定向至授权服务器
    AS-->>U: 要求用户登录并授权
    U->>AS: 同意授权
    AS-->>C: 4. 重定向并返回授权码 (code)
    Note over C: 截获此处的 code 也无用,<br/>因为攻击者没有 code_verifier

    C->>AS: 5. 交换令牌请求 <br/>(携带 code 和 code_verifier)
    Note over AS: 6. 验证 code_verifier 和 <br/>   之前存储的 code_challenge 是否匹配
    AS-->>C: 验证通过,返回 Access Token

    C->>RS: 7. 使用 Access Token 请求资源
    RS-->>C: 返回受保护的资源

# 发起授权请求

客户端首先生成一个随机的 code_verifier ,将这个 code_verifier 存储在本地(例如,App 的内存或浏览器的 sessionStorage 中),根据 code_verifier 计算出对应的 code_challenge

code_challengecode_verifier 先按照 code_challenge_method 进行计算,然后进行 Base64URL 编码

然后构建授权请求链接:

Base URL :授权服务器的授权 Endpoint,例如 https://xxx.com/api/authorize
response_typecode
client_id :客户端 ID
redirect_uri :回调 URL
scope :资源(权限)范围,例如 openidemailprofile
state :随机字符,可选(推荐),用于防止跨站请求伪造(CSRF)攻击
code_challenge :第一步通过 code_verifier 计算出的 code_challenge
code_challenge_methodS256 (具体看授权服务器的支持类型)

授权服务器收到请求后,会存储 code_challenge 并将其与即将生成的授权码关联起来。

# 确认授权

用户在授权服务器的页面上登录,并同意授权给客户端。

然后重定向至 redirect_uri,并附带 codestate 内容

# 客户端请求 access_token

与标准的授权码模式相比,请求体中必须额外包含第一步生成的 code_verifier

Base URL:授权服务器的令牌 Endpoint,例如 https://xxx.com/api/token

客户端向 Base URL 发送 POST 请求,请求体 Content-Typeapplication/x-www-form-urlencoded ,请求体内容如下:

grant_typeauthorization_code
code :上文的返回的 code
redirect_uri :回调 URL
client_id :客户端 ID
code_verifier :第一步生成的 code_verifier

收到请求后,授权服务器计算 code_verifier 在使用算法(S256)后是否与开始提供的 code_challenge 一致,一致则下发 access_token

# 客户端请求资源

当授权服务器返回 access_token 后,客户端使用 access_token 向资源服务器请求所需要的资源

资源服务器对 access_token 进行一系列校验,确认身份后返回客户端所需要的资源。

# 设备码流程 (Device Code Flow)

设备码流程(Device Code Flow)非常巧妙地解决了那些没有浏览器或输入不方便的设备(比如智能电视、游戏机、命令行工具、树莓派等)如何进行 OAuth 2.0 授权的问题。

整个流程的核心思想是:在输入受限的设备上发起授权,然后在另一个功能齐全的设备(如手机或电脑)上完成授权确认

sequenceDiagram
    participant TV as 客户端设备 (智能电视)
    participant User as 用户
    participant AS as 授权服务器
    participant Phone as 授权设备 (手机)

    TV->>AS: 请求设备码和用户码
    AS-->>TV: 返回 device_code, user_code 等

    TV->>User: 显示 user_code 和验证网址

    User->>Phone: 在手机上访问网址
    Phone->>AS: 提交 user_code 并登录授权

    Note over TV, AS: 设备在后台持续轮询, 直到用户授权成功

    AS-->>TV: (轮询成功后) 返回 Access Token

    TV->>User: 显示登录成功

# 设备发起授权请求

设备向授权服务器的一个特定端点 —— 设备授权端点(Device Authorization Endpoint)发起 POST 请求,

Base URL:授权服务器的设备授权 Endpoint,例如 https://xxx.com/api/device_authorization

请求体 Content-Typeapplication/x-www-form-urlencoded ,请求体内容如下:

client_id :客户端 ID
scope :资源(权限)范围,例如 openidemailprofile

授权服务器将返回临时凭证

示例:

1
2
3
4
5
6
7
8
{
"device_code": "GmRhmhtR4Q1d74VklbV_g9s-265aKqrC",
"user_code": "WDJB-MJHT",
"verification_uri": "https://example.com/activate",
"verification_uri_complete": "https://example.com/activate?user_code=WDJB-MJHT",
"expires_in": 1800,
"interval": 5
}

device_code :设备代码,是保存在设备上的代码
verification_uri :显示给最终用户以输入代码的 URL
verification_uri_complete :与上述相同的 URL,不同之处在于代码已被预填充,用户无需再输入代码
user_code :最终用户输入的原始代码
expires_in :此令牌将在多少秒后过期
interval :设备应多久检查一次令牌状态的时间间隔(以秒为单位)

# 设备端显示信息并开始轮询

设备开始向用户展示 verification_uriuser_code 并引导用户访问该网址,填写代码

感觉和 HMCL 的登录 MC 正版账户流程很像啊

然后设备开始轮询,按照建议的 interval ,持续向授权服务器的令牌 Endpoint 发起 POST 请求。

请求体 Content-Type 同样为 application/x-www-form-urlencoded ,请求体内容如下:

grant_typeurn:ietf:params:oauth:grant-type:device_code (设备码流程专用)
device_code :上文的 device_code
client_id :客户端 ID

需要注意的是,部分授权服务器返回的 device_code 过于抽象(说的就是你 authentik),可能会内嵌部分需要转义的字符,因此返回的 device_code 内容是已经加了 \ 转义符的,但是该转义符不属于 device_code 内容,如果设备对于发出请求的内容自带转义将会使 \ 也被提交,导致出现 invalid_grant 错误!

# 确认授权

用户访问展示的 verification_uri ,并填入 user_code ,在授权页面确认授权

当然也可以直接展示 verification_uri_complete

# 设备获取 access_token

在用户同意授权后,下一次轮询将获得授权服务器下发的 access_token

# 设备请求资源

当授权服务器返回 access_token 后,设备使用 access_token 向资源服务器请求所需要的资源

资源服务器对 access_token 进行一系列校验,确认身份后返回客户端所需要的资源。

# 总结

隐式授权模式和密码凭证模式这两个不安全的模式就不写了。

授权模式 适用场景 安全性 推荐度
授权码 + PKCE 所有类型的应用,特别是公共客户端(SPA、移动应用)和机密客户端(Web 应用) 非常高 强烈推荐
授权码模式 有后端的 Web 应用(机密客户端) 推荐
客户端凭证模式 机器对机器(M2M)通信,无用户参与 推荐
设备码流程 无浏览器或输入受限的设备 中高 特定场景推荐
隐式授权模式 已废弃 不推荐
密码凭证模式 已废弃,仅用于高度信任的遗留系统 非常低 不推荐

# 后记

暑假意外的高产😁