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

5.2 KiB
Raw Blame History

tags, create time
tags create time
sso
oauth2
auth
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 来补上这一环。

核心流程

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

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 实现授权码流程的核心步骤:

// 配置 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 越少,信任度越高