4. OAuth2.0 授权码流程详解

授权码流程(Authorization Code Flow)是 OAuth2.0 中最安全、最常用的授权方式,特别适用于有后端服务的 Web 应用。该流程通过引入一个临时的“授权码”作为中间凭证,确保访问令牌(Access Token)不会暴露在用户代理(浏览器)中,从而有效降低令牌泄露风险。

整个流程可拆解为四个核心阶段:授权码请求、用户授权、授权码交换令牌、访问资源。下面将逐一深入解析每个阶段的交互细节与安全要点。

4.1 授权码请求(Authorization Request)

客户端(Client)引导资源所有者(User)访问授权服务器(Authorization Server)的授权端点,发起授权请求。此阶段发生在用户浏览器中,客户端通过 HTTP 重定向将用户引导至授权服务器。

关键请求参数:
  • response_type=code —— 固定值,表示使用授权码流程。
  • client_id —— 客户端在授权服务器注册时获得的唯一标识。
  • redirect_uri —— 授权服务器在用户授权后重定向回客户端的地址。
  • scope —— 请求的权限范围,如 openid profile email。
  • state —— 客户端生成的随机字符串,用于防止 CSRF 攻击。
// 构造授权请求 URL(示例)
authURL := fmt.Sprintf(
    "https://auth.example.com/oauth2/authorize?response_type=code&client_id=%s&redirect_uri=%s&scope=openid+profile&state=%s",
    url.QueryEscape(clientID),
    url.QueryEscape(redirectURI),
    url.QueryEscape(state),
)
// 重定向用户浏览器至授权服务器
http.Redirect(w, r, authURL, http.StatusFound)
安全提示:state 参数必须由服务端生成并存储在会话中,授权回调时需严格校验。缺少 state 校验的客户端极易遭受 CSRF 授权劫持攻击。

4.2 用户授权(User Authorization)

资源所有者(用户)在授权服务器上完成身份认证后,决定是否授权客户端请求的权限。授权服务器展示授权页面,列出客户端名称、请求的权限范围等信息,用户点击“允许”或“拒绝”。

用户授权后,授权服务器生成一个临时的授权码,并通过重定向将用户浏览器送回客户端指定的 redirect_uri,授权码附加在 URL 查询参数中。

// 授权回调 URL 示例(授权服务器重定向回客户端)
// https://client.example.com/callback?code=AUTH_CODE_XYZ&state=CLIENT_STATE_ABC
关键约束:授权码具有以下安全属性:
  • 一次性使用:授权码只能使用一次,使用后立即失效。
  • 短生命周期:通常有效期为 1~10 分钟,过期后无法交换令牌。
  • 绑定客户端:授权码必须与发起请求的 client_id 和 redirect_uri 绑定。

4.3 授权码交换令牌(Token Exchange)

客户端收到授权码后,在后端服务端直接向授权服务器的令牌端点(Token Endpoint)发起 POST 请求,用授权码换取访问令牌(Access Token)和可选的刷新令牌(Refresh Token)。此阶段不经过用户浏览器,令牌不会暴露给用户代理。

参数 说明 示例值
grant_type 授权类型,固定为 authorization_code authorization_code
code 上一步获取的授权码 AUTH_CODE_XYZ
redirect_uri 必须与授权请求中的 redirect_uri 一致 https://client.example.com/callback
client_id 客户端标识 my-web-app
client_secret 客户端密钥,用于验证客户端身份 s3cr3t!
// Go 后端发起令牌交换请求
tokenURL := "https://auth.example.com/oauth2/token"
data := url.Values{
    "grant_type":   {"authorization_code"},
    "code":         {authCode},
    "redirect_uri": {redirectURI},
    "client_id":    {clientID},
    "client_secret": {clientSecret},
}

resp, err := http.PostForm(tokenURL, data)
if err != nil {
    // 处理错误
}
defer resp.Body.Close()

// 解析响应 JSON 获取 access_token
var tokenResp struct {
    AccessToken  string `json:"access_token"`
    TokenType    string `json:"token_type"`
    ExpiresIn    int    `json:"expires_in"`
    RefreshToken string `json:"refresh_token,omitempty"`
    IDToken      string `json:"id_token,omitempty"`
}
json.NewDecoder(resp.Body).Decode(&tokenResp)
并发安全处理:在 Go 微服务中,令牌交换请求可能被多个 goroutine 同时触发。建议使用 sync.Mutex 或 singleflight 包防止重复的令牌交换请求,避免因并发导致授权码被重复使用而报错。

4.4 访问资源(Access Resource)

客户端获得访问令牌后,在请求受保护资源时,将令牌放入 HTTP 请求的 Authorization 头中,以 Bearer 令牌的形式发送给资源服务器(Resource Server)。资源服务器验证令牌的有效性、作用域和过期时间后,返回请求的资源数据。

// 使用访问令牌访问受保护资源
req, _ := http.NewRequest("GET", "https://api.example.com/user/profile", nil)
req.Header.Set("Authorization", "Bearer "+tokenResp.AccessToken)

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
    // 处理错误
}
defer resp.Body.Close()

// 读取资源响应
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
令牌安全存储:访问令牌和刷新令牌必须存储在服务端安全区域(如加密数据库、内存缓存),严禁存储在浏览器 Cookie、localStorage 或 URL 参数中。令牌泄露等同于账户泄露。
OAuth2.0 授权码流程完整交互图 用户(资源所有者) 客户端(Web 应用) 授权服务器 资源服务器 ① 授权码请求 ② 用户授权 ③ 返回授权码 ④ 交换令牌 ⑤ 返回访问令牌 ⑥ 访问资源 ⑦ 返回资源数据

4.5 流程安全要点总结

阶段 安全要点 常见风险
授权码请求 使用 state 参数防 CSRF;验证 redirect_uri 白名单 CSRF 攻击、开放重定向
用户授权 授权码一次性、短生命周期、绑定客户端 授权码截获、重放攻击
交换令牌 服务端到服务端通信;使用 client_secret 或 PKCE 验证 授权码泄露、中间人攻击
访问资源 Bearer 令牌仅用于服务端;令牌加密存储 令牌泄露、令牌劫持
Go 微服务最佳实践:
  • 使用 golang.org/x/oauth2 标准库简化授权码流程实现。
  • 令牌交换请求添加超时控制(context.WithTimeout),避免 goroutine 泄漏。
  • 令牌缓存使用 sync.RWMutex 或 singleflight.Group 防止缓存击穿。
  • 资源服务器验证令牌时,优先使用 JWKS 端点本地验证 JWT,减少对授权服务器的依赖。