4、授权码流程详解:授权请求、用户认证、授权码发放、令牌交换、令牌刷新

授权码流程(Authorization Code Flow)是 OAuth 2.0 中最安全、最常用的授权模式,特别适用于有后端服务的 Web 应用。该流程通过引入一个临时的“授权码”作为中间凭证,确保访问令牌(Access Token)和刷新令牌(Refresh Token)只在安全的服务端通道中传输,避免令牌暴露在用户代理(浏览器)中。

整个流程可拆解为五个核心阶段:授权请求、用户认证、授权码发放、令牌交换、令牌刷新。下面将逐一深入解析每个阶段的交互细节、关键参数及安全考量。

4.1 流程全景图

下图展示了授权码流程的完整交互链路,涉及资源所有者(用户)、用户代理(浏览器)、客户端应用、授权服务器、资源服务器五个参与方。

授权码流程全景图 授权码流程全景图 资源所有者 用户代理 客户端应用 授权服务器 资源服务器 ① 授权请求 ② 重定向至授权端点 ③ 用户认证 ④ 授权码 ⑤ 令牌交换 ⑥ 访问令牌 ⑦ 访问资源 ⑧ 资源响应 关键安全设计: • 授权码为一次性临时凭证,有效期通常为 5-10 分钟,即使被截获也无法长期使用。 • 令牌交换阶段要求客户端提供 client_secret,确保只有合法客户端能兑换令牌。 • 访问令牌和刷新令牌仅在服务端间传输,不经过用户代理,降低泄露风险。

4.2 授权请求(Authorization Request)

授权请求是流程的起点。客户端应用构造一个 URL,将用户代理重定向到授权服务器的授权端点(Authorization Endpoint)。该请求通常以 HTTP GET 方式发起,包含以下关键参数:

参数 是否必需 说明 示例值
response_type 是 固定为 code,表示使用授权码流程 code
client_id 是 客户端在授权服务器注册时获得的唯一标识 app_123456
redirect_uri 是 授权码发放后重定向回客户端的地址,必须与注册时一致 https://myapp.com/callback
scope 否 请求的权限范围,多个 scope 用空格分隔 openid profile email
state 强烈推荐 防 CSRF 攻击的随机字符串,回调时原样返回用于校验 xyz789

安全要点:state 参数是防止跨站请求伪造(CSRF)的关键手段。客户端应生成不可预测的随机值,并在回调验证时确保与发送时一致。Go 中可使用 crypto/rand 生成 32 字节随机数并 Base64 编码。

构造完成的授权请求 URL 示例如下:

GET /authorize?
  response_type=code&
  client_id=app_123456&
  redirect_uri=https%3A%2F%2Fmyapp.com%2Fcallback&
  scope=openid+profile&
  state=xyz789
HTTP/1.1
Host: auth.example.com

4.3 用户认证(User Authentication)

授权服务器收到授权请求后,首先检查用户是否已登录。若未登录,则展示登录页面要求用户提供凭证(用户名/密码、MFA 等)。认证成功后,授权服务器会展示授权页面,告知用户客户端请求的权限范围,并请求用户确认授权。

用户认证阶段的关键交互步骤:

  1. 检查登录状态:授权服务器通过 Session 或 Cookie 判断用户是否已认证。
  2. 用户登录:若未认证,重定向至登录页面,用户提交凭证。
  3. 权限确认:展示客户端请求的 scope 列表,用户点击“允许”或“拒绝”。
  4. 生成授权码:用户确认后,授权服务器生成一次性授权码。

注意:用户认证过程完全在授权服务器控制下进行,客户端无法直接接触用户密码。这是 OAuth 2.0 的核心安全设计——客户端不存储用户凭证,仅通过令牌访问资源。

4.4 授权码发放(Authorization Code Grant)

用户确认授权后,授权服务器生成一个临时的授权码,并通过 HTTP 302 重定向将用户代理送回客户端指定的 redirect_uri。授权码附加在 URL 的 query 参数中:

HTTP/1.1 302 Found
Location: https://myapp.com/callback?code=AUTH_CODE_123&state=xyz789

授权码具有以下特性:

  • 一次性:使用后立即失效,不可重复兑换。
  • 短有效期:通常为 5-10 分钟,过期后需重新发起授权请求。
  • 绑定客户端:授权码与特定 client_id 和 redirect_uri 绑定,防止被其他客户端使用。
  • 不包含用户信息:授权码本身不携带用户身份信息,仅作为兑换令牌的凭证。

客户端收到回调后,必须验证 state 参数与之前发送的值是否一致,防止 CSRF 攻击。

4.5 令牌交换(Token Exchange)

客户端拿到授权码后,在服务端直接向授权服务器的令牌端点(Token Endpoint)发起 POST 请求,交换访问令牌和可选的刷新令牌。此步骤在客户端后端完成,不经过用户代理,确保令牌传输安全。

令牌交换请求包含以下参数:

参数 是否必需 说明
grant_type 是 固定为 authorization_code
code 是 上一步获取的授权码
redirect_uri 是 必须与授权请求中的 redirect_uri 一致
client_id 是 客户端标识
client_secret 是 客户端密钥,用于验证客户端身份

Go 中发起令牌交换请求的示例代码:

package main

import (
    "encoding/json"
    "net/http"
    "net/url"
    "strings"
)

type TokenResponse struct {
    AccessToken  string `json:"access_token"`
    TokenType    string `json:"token_type"`
    ExpiresIn    int    `json:"expires_in"`
    RefreshToken string `json:"refresh_token,omitempty"`
    Scope        string `json:"scope,omitempty"`
}

func exchangeCodeForToken(clientID, clientSecret, code, redirectURI, tokenEndpoint string) (*TokenResponse, error) {
    data := url.Values{
        "grant_type":   {"authorization_code"},
        "code":         {code},
        "redirect_uri": {redirectURI},
        "client_id":    {clientID},
        "client_secret": {clientSecret},
    }

    resp, err := http.PostForm(tokenEndpoint, data)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var token TokenResponse
    if err := json.NewDecoder(resp.Body).Decode(&token); err != nil {
        return nil, err
    }
    return &token, nil
}

授权服务器验证通过后,返回 JSON 格式的令牌响应:

{
  "access_token": "ACCESS_TOKEN_VALUE",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "REFRESH_TOKEN_VALUE",
  "scope": "openid profile"
}

4.6 令牌刷新(Token Refresh)

访问令牌通常具有较短的有效期(如 1 小时),过期后客户端需要使用刷新令牌获取新的访问令牌,避免用户频繁重新授权。刷新令牌具有更长的有效期(如 30 天),且可以多次使用。

令牌刷新请求同样发往令牌端点,grant_type 参数改为 refresh_token:

参数 是否必需 说明
grant_type 是 固定为 refresh_token
refresh_token 是 之前获取的刷新令牌
client_id 是 客户端标识
client_secret 是 客户端密钥
scope 否 可选,若省略则保持原有 scope

Go 中实现令牌刷新的示例:

func refreshAccessToken(clientID, clientSecret, refreshToken, tokenEndpoint string) (*TokenResponse, error) {
    data := url.Values{
        "grant_type":    {"refresh_token"},
        "refresh_token": {refreshToken},
        "client_id":     {clientID},
        "client_secret": {clientSecret},
    }

    resp, err := http.PostForm(tokenEndpoint, data)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var token TokenResponse
    if err := json.NewDecoder(resp.Body).Decode(&token); err != nil {
        return nil, err
    }
    return &token, nil
}

最佳实践:刷新令牌应安全存储在服务端,避免泄露。Go 中可使用环境变量或安全的密钥管理服务(如 HashiCorp Vault)存储客户端凭据。同时,建议在刷新令牌时检查返回的 scope 是否与预期一致,防止权限被篡改。

4.7 令牌刷新与错误处理集成

在实际系统中,令牌刷新通常与错误处理逻辑集成。当客户端调用资源服务器 API 返回 401 Unauthorized 时,自动触发令牌刷新流程,然后重试原始请求。这种模式称为“自动令牌刷新中间件”。

下图展示了令牌刷新与错误处理的集成流程:

令牌刷新与错误处理集成流程 令牌刷新与错误处理集成流程 发起 API 请求 检查响应状态码 返回成功响应 触发令牌刷新 重试原始请求 返回最终错误 200 OK 401 Unauthorized 刷新失败 重试 说明:虚线表示重试路径,刷新令牌成功后自动重试原始请求;刷新失败则返回最终错误。

在 Go 中实现自动刷新中间件的核心逻辑:

type AutoRefreshTransport struct {
    Transport     http.RoundTripper
    TokenEndpoint string
    ClientID      string
    ClientSecret  string
    RefreshToken  string
    AccessToken   string
}

func (t *AutoRefreshTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    // 设置当前访问令牌
    req.Header.Set("Authorization", "Bearer "+t.AccessToken)

    resp, err := t.Transport.RoundTrip(req)
    if err != nil {
        return nil, err
    }

    // 检查 401 状态码,触发刷新
    if resp.StatusCode == http.StatusUnauthorized {
        resp.Body.Close()

        // 刷新令牌
        newToken, err := refreshAccessToken(t.ClientID, t.ClientSecret, t.RefreshToken, t.TokenEndpoint)
        if err != nil {
            return nil, err
        }

        // 更新令牌
        t.AccessToken = newToken.AccessToken
        if newToken.RefreshToken != "" {
            t.RefreshToken = newToken.RefreshToken
        }

        // 重试原始请求
        newReq := req.Clone(req.Context())
        newReq.Header.Set("Authorization", "Bearer "+t.AccessToken)
        return t.Transport.RoundTrip(newReq)
    }

    return resp, nil
}

并发安全提示:在多 goroutine 环境下,AccessToken 和 RefreshToken 的读写需要加锁保护。推荐使用 sync.RWMutex 或 sync.Map 确保并发安全。后续章节将深入讲解如何在 Go 并发中间件中安全地管理令牌状态。

4.8 流程对比与总结

下表总结了授权码流程五个阶段的核心特征:

阶段 参与方 传输通道 关键凭证 安全风险
授权请求 客户端 → 授权服务器 用户代理(浏览器) client_id、state CSRF(通过 state 防范)
用户认证 用户 ↔ 授权服务器 用户代理(浏览器) 用户凭证 钓鱼攻击(通过 HTTPS 防范)
授权码发放 授权服务器 → 客户端 用户代理(浏览器) 授权码 授权码截获(短有效期 + 一次性)
令牌交换 客户端 ↔ 授权服务器 服务端直连 client_secret、授权码 中间人攻击(通过 HTTPS + 客户端认证)
令牌刷新 客户端 ↔ 授权服务器 服务端直连 刷新令牌、client_secret 刷新令牌泄露(安全存储 + 轮换策略)