4、授权码流程详解:授权请求、用户认证、授权码发放、令牌交换、令牌刷新
授权码流程(Authorization Code Flow)是 OAuth 2.0 中最安全、最常用的授权模式,特别适用于有后端服务的 Web 应用。该流程通过引入一个临时的“授权码”作为中间凭证,确保访问令牌(Access Token)和刷新令牌(Refresh Token)只在安全的服务端通道中传输,避免令牌暴露在用户代理(浏览器)中。
整个流程可拆解为五个核心阶段:授权请求、用户认证、授权码发放、令牌交换、令牌刷新。下面将逐一深入解析每个阶段的交互细节、关键参数及安全考量。
4.1 流程全景图
下图展示了授权码流程的完整交互链路,涉及资源所有者(用户)、用户代理(浏览器)、客户端应用、授权服务器、资源服务器五个参与方。
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 等)。认证成功后,授权服务器会展示授权页面,告知用户客户端请求的权限范围,并请求用户确认授权。
用户认证阶段的关键交互步骤:
- 检查登录状态:授权服务器通过 Session 或 Cookie 判断用户是否已认证。
- 用户登录:若未认证,重定向至登录页面,用户提交凭证。
- 权限确认:展示客户端请求的 scope 列表,用户点击“允许”或“拒绝”。
- 生成授权码:用户确认后,授权服务器生成一次性授权码。
注意:用户认证过程完全在授权服务器控制下进行,客户端无法直接接触用户密码。这是 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 时,自动触发令牌刷新流程,然后重试原始请求。这种模式称为“自动令牌刷新中间件”。
下图展示了令牌刷新与错误处理的集成流程:
在 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 |
刷新令牌泄露(安全存储 + 轮换策略) |