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