CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
OAuth 2.0 与 OpenID Connect/课时/授权码流
RawBlame
课时目录12 节

OAuth 2.0 与 OpenID Connect

  1. 01OAuth 是什么
  2. 02授权码流
  3. 03PKCE 与 SPA
  4. 04OpenID Connect
  5. 05Scope 与权限
  6. 06Refresh Token
  7. 07Spring OAuth2 Client
  8. 08Resource Server
  9. 09第三方登录
  10. 10OAuth vs 本站 JWT 登录
  11. 11常见安全问题
  12. 12综合练习

授权码流

Authorization Code、redirect、state 防 CSRF。

35 分钟

授权码流

这一课你将能做到

  1. 按顺序说出授权码流(Authorization Code Flow)的五个环节:跳转 authorize → 用户登录并同意 → callback 带一次性 code → 后端用 code 换 token → 拿 Bearer token 调资源 API
  2. 解释 state 参数如何防 CSRF,并写出带校验逻辑的 callback 处理代码
  3. 背出四种授权模式的差异与现状,说清为什么新系统一律选授权码(+PKCE)

为什么要学

你迟早会问:既然最后要的是 access_token,为什么不一步到位,让授权服务器直接把 token 发回来?

因为浏览器是公共信道。如果 token 出现在 URL 里,它会流进浏览器历史、服务器访问日志、Referer 头、甚至浏览器插件的眼睛里。授权码流的聪明之处在于:让浏览器只经手一枚 一次性、几分钟内作废、离开 secret 或 PKCE 就毫无用处 的 code;真正的 token 走 后端与授权服务器之间的服务端到服务端请求,全程不出现在前端。

几乎所有的「GitHub / Google / 微信登录」按钮、Spring OAuth2 Client 的默认流程,跑的都是这条路。把这一课的五步吃透,后面 PKCE、OIDC、Spring Client 都只是在这条主干上加零件。

面试关系:「四种授权模式的区别」「为什么授权码最安全」是后端高频题,本课给到能直接背诵的程度。

先建立图像

浏览器                 你的后端(Client)          授权服务器(AS)         资源服务器(RS)
   │ ① GET /oauth/start(后端拼 authorize URL)│                     │
   │ ──────────────────────────────────────► │                     │
   │ ◄──── 302 Location: AS 的授权页 ─────────│                     │
   │                                         │                     │
   │ ② 浏览器打开授权页,用户登录 + 点「同意」   │                     │
   │                                         │                     │
   │ ◄──── 302 回 redirect_uri?code=xxx&state=yyy ────────────────── │
   │                                         │                     │
   │ ③ GET /oauth/callback?code=xxx&state=yyy│                     │
   │ ──────────────────────────────────────► │                     │
   │                        ④ POST /token(code + client_secret)
   │                        │ ─────────────────►│                    │
   │                        │ ◄── access_token (+ refresh) ─────────│
   │                        │ ⑤ GET /user  Authorization: Bearer xxx│
   │                        │ ─────────────────────────────────────►│
   │ ◄── 302 回前端,带上本站 JWT ──│                                │

authorize 请求的五个核心参数:

参数人话
response_type=code我要的是授权码,别直接塞 token 给我
client_id哪个应用在申请(公开可见,不是机密)
redirect_uri同意后精确跳回哪里(必须预登记)
scope申请哪些权限
state一次性随机串,回调时原样带回,防伪造

跟着做

步骤 1:手拼一条 authorize URL

GET https://github.com/login/oauth/authorize
  ?client_id=你的ClientID
  &redirect_uri=http://localhost:8080/login/oauth2/code/github
  &scope=read:user
  &state=9f8c2b1e7d4a(加密安全随机串)

在浏览器打开它,应看到 GitHub 授权页。注意:此时还没有任何 token,浏览器只带了「申请意向」。

步骤 2:发起跳转前,把 state 存进服务端会话

// 中文注释:发起 OAuth 前生成 state 存入 session;callback 必须原样比对
@GetMapping("/oauth/start")
public void startOAuth(HttpSession session, HttpServletResponse response) throws IOException {
    String state = UUID.randomUUID().toString().replace("-", "");
    session.setAttribute("oauth_state", state);

    String authorizeUrl = "https://github.com/login/oauth/authorize"
        + "?client_id=" + clientId
        + "&redirect_uri=" + URLEncoder.encode(redirectUri, StandardCharsets.UTF_8)
        + "&scope=read:user"
        + "&state=" + state;
    response.sendRedirect(authorizeUrl);
}

步骤 3:callback 里先验 state,再考虑换 token

// 中文注释:回调第一步永远是验 state;不匹配直接丢弃 code,绝不继续
@GetMapping("/oauth/callback")
public String callback(@RequestParam String code,
                       @RequestParam(required = false) String state,
                       HttpSession session) {
    String expected = (String) session.getAttribute("oauth_state");
    session.removeAttribute("oauth_state");   // 一次性,防重放
    if (expected == null || state == null || !MessageDigest.isEqual(
            expected.getBytes(StandardCharsets.UTF_8),
            state.getBytes(StandardCharsets.UTF_8))) {
        throw new IllegalStateException("state 校验失败,疑似 CSRF,丢弃 code");
    }
    // 校验通过,才进入下一步:拿 code 去换 token(服务端行为)
    return "redirect:/oauth/exchange?code=" + code;
}

用 MessageDigest.isEqual 做常量时间比较,比 equals 更严谨(防时序侧信道)。

步骤 4:curl 模拟换 token

# 中文注释:换 token 是服务端到服务端的 POST;secret 只放环境变量
curl -i -X POST https://github.com/login/oauth/access_token \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "client_id": "你的ClientID",
    "client_secret": "从环境变量读取,绝不进前端",
    "code": "callback里收到的一次性code",
    "redirect_uri": "http://localhost:8080/login/oauth2/code/github"
  }'

期望响应:

{
  "access_token": "gho_xxxxxxxx",
  "token_type": "bearer",
  "scope": "read:user"
}

要点:redirect_uri 必须与 authorize 时字节级一致;code 是一次性的,第二次使用同一 code 必然报错。

步骤 5:拿 token 调资源

# 中文注释:资源服务器只认 Bearer 头,不认你站内 cookie
curl -i https://api.github.com/user \
  -H "Authorization: Bearer gho_刚换到的token"

返回用户 JSON。401 = token 缺失/无效;403 = token 有效但 scope 不够(第 5 课细讲)。

步骤 6:对照 Spring OAuth2 Client 的默认路径

# 中文注释:Spring 把五步全部封装;先看懂流程再交给框架(第 7 课完整配置)
spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: ${GITHUB_CLIENT_ID}
            client-secret: ${GITHUB_CLIENT_SECRET}
            scope: read:user
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"

浏览器访问 http://localhost:8080/oauth2/authorization/github 即步骤 1;回调落在 /login/oauth2/code/github,state 校验与换 token 由框架代劳。

步骤 7:四种授权模式对照(面试背诵卡)

模式token 如何到达 Client现状适用
Authorization Code浏览器拿 code,后端换 token✅ 标准答案有后端的 Web 应用
Auth Code + PKCE浏览器拿 code,客户端带 verifier 换✅ 推荐SPA、移动 App
Implicittoken 直接放 URL fragment❌ 已废弃无(历史遗留)
Resource Owner Password用户把密码交给 Client,Client 拿密码换 token❌ 已不建议无(仅遗留内网)
Client Credentials应用用自身凭据换 token✅ 有效纯机器对机器,无用户参与

讲清楚要点

  1. 浏览器只配经手 code,不配经手 token。code 一次性、短寿命、离开 secret/PKCE 无效;token 走后端信道。这是授权码流安全性的全部核心。
  2. state 防 CSRF:攻击者可诱导已登录用户的浏览器带着「攻击者的 code」访问你的 callback(login CSRF)。state 与会话绑定且一次性,回调时严格比对后立即删除。
  3. redirect_uri 要传两次:authorize 一次、换 token 一次,且必须完全一致。第二次是绑定「这张 code 只许这个回调方来换」,防止 code 被拿去别的 redirect 场景。
  4. code 换 token 必须带凭据:机密客户端带 client_secret;公共客户端带 code_verifier(下一课 PKCE)。
  5. token 响应里的 scope 字段是「实际批准值」:可能少于申请值(第 5 课),客户端必须以响应为准。
  6. Implicit 被废弃的原因:token 直接暴露在 URL fragment,会被历史/插件/Referer 收集,且没有 refresh、没有后端确认环节。OAuth 2.1 草案已把「授权码 + PKCE」定为浏览器与移动端唯一推荐。

你可能会懵的地方

  • code 也出现在 URL 里,它和「token 在 URL 里」有什么本质区别? 危险等级不同。code 一次性且短寿命,没有 secret/verifier 换不到 token,被截获损失≈0;Implicit 是把长效 token 直接放在 fragment 里,截获即拿到资源访问权。

  • state 存哪里?前端 localStorage 行不行? 优先存服务端 session(或 Redis,key 绑定会话标识)。放 localStorage 意味着 XSS 可读取篡改,削弱防护。

  • 为什么登录后浏览器一直循环跳转? 多半是 callback 路径又被 Security 拦去「未认证跳转」,形成「callback → 登录 → callback」循环。把 callback 与授权入口 permitAll,见卡点急救。

  • 换 token 时能把 client_secret 放 query 参数吗? 不能。规范要求放 POST body 或 Basic 认证头;query 会被日志与代理记录。secret 只应出现在加密信道内。

  • 一个 code 换了两次 token,第二次报错,是不是 bug? 不是。code 设计为一次性,重放必须失败。若线上频繁出现,检查是否网络层自动重试导致重复提交。

错法对照

错法问题正解
callback 收到 code 就在前端直接换 tokensecret 暴露或退化为无保护交换服务端换 token;SPA 走 PKCE
state 用固定字符串或不校验CSRF 防护形同虚设每次生成随机 state,校验后立即作废
redirect_uri 允许通配或随意追加 query开放重定向可把 code 送给攻击者预注册精确 URI,全等匹配
把 code 写进访问日志长期保存日志泄露 = code 泄露code 即换即弃,日志打码
继续实现 Implicit「图省事」token 暴露面过大授权码 + PKCE

卡点急救

  1. 现象:授权后报 redirect_uri_mismatch。三处逐字比对:authorize URL 里的、IdP 后台登记的、换 token 请求里的。最常见是尾部斜杠多/少、http 与 https 混用、localhost 与 127.0.0.1 混用。
  2. 现象:换 token 返回 invalid_grant。按概率排查:code 已被用过(一次性);code 已过期(通常 1-10 分钟);redirect_uri 与 authorize 时不一致;服务器时钟偏差。处理:重新走完整授权流程,不要重试旧 code。
  3. 现象:登录后无限循环重定向。callback 路径被 Security 拦截,未登录 → 又跳授权 → 又回 callback。解法:在 SecurityFilterChain 里把 /login/oauth2/code/**、/oauth/callback 等 callback 与授权入口 permitAll()。

面试常问

  • 四种授权模式的区别与选用? 见步骤 7 表格。回答时强调:有后端选授权码;SPA/移动选授权码+PKCE;纯服务间调用选 Client Credentials;Implicit 与密码模式已废弃,并答出「为什么废弃」(token 暴露在 URL / 用户密码交给第三方)即可拿满分。

  • 为什么授权码流是最安全的? 三点:① token 不经浏览器,走服务端信道;② code 一次性短寿命,泄露损失小;③ state + PKCE + 严格 redirect_uri 分别挡住 CSRF、code 拦截、开放重定向三类攻击。纵深防御。

  • state 校验为什么必须在换 token 之前? CSRF 的目的就是让你的服务器替攻击者完成「绑定」动作。若先换 token 再校验,攻击者的 code 已经变成你 session 里的登录态,校验晚了就失去意义。正确顺序:验 state → 换 token → 建立本地会话。

自测三问

  1. 从点击按钮到拿到用户资料,写出五个环节的 HTTP 请求类型与关键参数。
  2. state 防什么攻击?校验不匹配时代码应该保留还是丢弃 code?
  3. 为什么换 token 请求里还要再传一次 redirect_uri?

小结一句话

授权码流:浏览器拿一次性 code,后端换 token,state 防 CSRF,redirect_uri 严格一致——四道锁共同构成「最安全模式」。

延伸

  • RFC 6749 §4.1:Authorization Code Grant
  • oauth.net:Authorization Code
  • 下一课:PKCE——没有 client_secret 的 SPA 和移动 App 如何安全地走授权码流。
加载本节练习…
← 上一课OAuth 是什么下一课 →PKCE 与 SPA