Files
Qiniu/technical/sso/sso-oauth2.md
T

143 lines
5.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [sso, oauth2, auth]
create time: 2026-07-13 10:03
---
# OAuth 2.0 授权码模式
## 概述
OAuth 2.0 是一个**授权框架**(RFC 6749),设计初衷是让第三方应用在不获取用户密码的前提下,有限度地访问用户的资源。授权码模式(Authorization Code Grant)是最安全、最常用的流程,也是构建 SSO 的基础。
> [!info] OAuth 2.0 ≠ 认证协议
> OAuth 2.0 解决的是「**授权**」—— 让第三方拿走你的部分权限。它本身不提供用户身份信息,这也是为什么后来需要 OIDC 来补上这一环。
## 核心流程
```mermaid
sequenceDiagram
participant U as 用户 (浏览器)
participant App as Client App (SP)
participant Auth as Authorization Server (IdP)
participant Res as Resource Server
U->>App: 1. 点击「登录」
App->>U: 2. 302 重定向到 Auth
Note right of App: redirect_uri, scope, state, client_id
U->>Auth: 3. 登录 + 授权确认
Auth->>U: 4. 302 回调 redirect_uri
Note left of Auth: ?code=AUTH_CODE&state=xxx
U->>App: 5. 携带 code 回到 App
App->>Auth: 6. POST /token(后端请求)
Note right of App: code, client_id, client_secret, redirect_uri
Auth-->>App: 7. 返回 access_token
App->>Res: 8. 用 access_token 请求资源
Res-->>App: 9. 返回受保护数据
```
关键点:**步骤 2-5 走浏览器重定向,步骤 6-7 走后端 HTTPS 直连**。code 只能用一次,且必须在后端交换,避免 token 暴露在前端。
## 四种授权模式对比
| 模式 | 流程 | 安全性 | 适用场景 |
|------|------|--------|----------|
| **授权码模式** | 重定向 + 后端换 Token | 最高 | Web 后端应用、SSO |
| 授权码 + PKCE | 授权码 + 代码验证码 | 高 | SPA / 移动端(推荐) |
| 客户端凭证模式 | 直接换 Token | 中 | 服务间调用(M2M) |
| ~~隐式模式~~ | 直接返回 Token | 低 | **已弃用**,仅遗留系统 |
> [!warning] 隐式模式已被废弃
> RFC 9700 明确不推荐使用隐式模式。Token 暴露在 URL Fragment 中,容易被中间人和浏览器历史记录泄露。SPA 应使用 **PKCE** 模式替代。
## 核心概念速查
### 四个关键角色
| 角色 | 职责 | 示例 |
|------|------|------|
| Resource Owner | 资源拥有者(用户) | 登录的你 |
| Client | 第三方应用 | 公司的 Web 系统 |
| Authorization Server | 授权服务器 | Keycloak、Auth0 |
| Resource Server | 资源服务器 | 你的 API 服务 |
### 关键参数
| 参数 | 说明 |
|------|------|
| `client_id` | 应用注册时分配的标识 |
| `client_secret` | 应用密钥,仅后端持有 |
| `redirect_uri` | 回调地址,必须预注册 |
| `scope` | 请求的权限范围,如 `openid profile email` |
| `state` | 防 CSRF 的随机字符串,回调时原样返回 |
| `code` | 授权码,一次性,有效期通常 < 10 分钟 |
### Access Token vs Refresh Token
```mermaid
graph LR
A[Access Token] -->|有效期短 5-30min| B[访问 API]
C[Refresh Token] -->|有效期长 天/月| D[换取新 Access Token]
C -.->|轮换机制| C2[新 Refresh Token]
```
| Token | 用途 | 存储 | 有效期 |
|-------|------|------|--------|
| Access Token | 调用 API | 内存 / 安全 Cookie | 短(5-30 min) |
| Refresh Token | 刷新 Access Token | HttpOnly Cookie / 后端 | 长(天 ~ 月) |
> [!tip] Refresh Token Rotation
> 每次用 Refresh Token 换新 Token 时,同时颁发新的 Refresh Token 并使旧的失效。这样即使某个 Refresh Token 泄露,攻击者也只能用一次。
## Go 实现示例
使用 `golang.org/x/oauth2` 实现授权码流程的核心步骤:
```go
// 配置 OAuth2 客户端
conf := &oauth2.Config{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
Scopes: []string{"openid", "profile", "email"},
Endpoint: oauth2.Endpoint{
AuthURL: "https://idp.example.com/oauth/authorize",
TokenURL: "https://idp.example.com/oauth/token",
},
RedirectURL: "https://app.example.com/callback",
}
// 1. 生成授权 URL(附带 state 防 CSRF)
state := generateRandomState() // 需要存入 session
url := conf.AuthCodeURL(state, oauth2.AccessTypeOffline)
// 2. Callback 处理 — 用 code 换 Token
token, err := conf.Exchange(ctx, code)
if err != nil {
// 授权码过期或无效
log.Fatal(err)
}
// 3. 用 Token 请求用户信息
client := conf.Client(ctx, token)
resp, err := client.Get("https://idp.example.com/userinfo")
```
> `oauth2.AccessTypeOffline` 参数会请求颁发 Refresh Token。默认只发 Access Token。
## 常见陷阱与最佳实践
**CSRF 攻击**
- 必须使用 `state` 参数。生成随机值存入 session,回调时校验一致性
- 不用 `state` 就像进门不锁门
**Token 存储**
- 前端:不要存在 `localStorage`(XSS 可读取),用 HttpOnly Secure Cookie
- 后端:加密存储或存入 Session,不要写进数据库明文字段
**redirect_uri 必须严格匹配**
- 不支持通配符,不支持路径前缀匹配
- 生产环境不允许使用 `http://localhost`
**scope 最小化原则**
- 只请求你需要的权限,不要 `scope: "all"`
- 用户看到的授权页面 scope 越少,信任度越高