前言

本教程将结合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)通信,无用户参与 推荐
设备码流程 无浏览器或输入受限的设备 中高 特定场景推荐
隐式授权模式 已废弃 不推荐
密码凭证模式 已废弃,仅用于高度信任的遗留系统 非常低 不推荐

后记

暑假意外的高产😁