综合练习
这一课你将能做到
- 独立跑通最小闭环:GitHub OAuth App → Spring OAuth2 Client → 映射本地用户 → 签发本站 JWT → 前端 callback 落 cookie
- 可选分支:为
/api/v1/** 挂上 Resource Server,用带 tracks:read 的 access token 读接口,验证 401/403 行为
- 交出验收清单:密码轨与 OAuth 轨互不破坏、安全项全勾、README 写清「何时用哪种 token」
为什么要学
前 11 课拆零件,这一课装整机。判断你是否真学会的标准,不是能背多少术语,而是:
- 点一下按钮,能从 platform 跳到 GitHub 再带着本站会话回来;
- Network 里每一条请求的「角色」你都说得清:这是 authorize、这是 callback、这是换票、这是 RS 验签;
- 在仓库里搜
SECRET 搜不到真实值;
- 无 token 401、缺 scope 403、关掉 OAuth 密码登录照常工作。
做完这个练习,backend 的 OAuth 实验节点就可以打勾,并留下一段「何时用哪种 token」的团队文档——这是从「学过」到「用过」的分界线。
先建立图像
[Next platform]「GitHub 登录」按钮
│ window.location → backend
▼
[Spring] /oauth2/authorization/github ← Client 链:生成 state、302
▼
[GitHub IdP] 登录 + 同意
▼
[Spring] /login/oauth2/code/github ← 框架验 state、换 token、拉用户
│ OAuth2LoginSuccessHandler
│ findOrCreate(provider, externalId)
│ jwtUtil.sign(uid, role) ← 本站自研 JWT
▼
[Next] /login/callback?token=... ← AuthProvider 写 HttpOnly cookie
▼
后续所有请求 = 与密码登录完全相同的站内凭证轨道
可选分支:
[外部 Client] Bearer(IdP 签发, 含 tracks:read) → /api/v1/tracks(Resource Server 链)
跟着做
步骤 1:IdP 注册
GitHub → Developer settings → OAuth Apps → New
Homepage: http://localhost:3000
Callback URL: http://localhost:8080/login/oauth2/code/github
Client ID / Secret → 写入环境变量 GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET
自检点:Callback URL 与 Spring 默认路径逐字一致;secret 未出现在任何将被提交的文件里。
步骤 2:Client 配置与成功处理器
spring:
security:
oauth2:
client:
registration:
github:
client-id: ${GITHUB_CLIENT_ID}
client-secret: ${GITHUB_CLIENT_SECRET}
scope: read:user,user:email
实现 OAuth2LoginSuccessHandler(完整代码见第 7 课):findOrCreateFromOAuth("github", externalId, attrs) → jwtUtil.sign(...) → 302 回 frontendUrl + "/login/callback?token=" + URLEncoder.encode(token, UTF_8)。
启动 backend,浏览器访问 http://localhost:8080/oauth2/authorization/github,应能走完授权并回到前端。
步骤 3:前端闭环
登录页加「GitHub 登录」按钮 → 跳 backend 授权入口
/login/callback 页:读取 token → AuthProvider 写入与密码登录相同的 cookie → 跳首页
验收:刷新后仍显示已登录;带 cookie 调学员/管理接口返回 200
步骤 4:安全门禁自检(逐项打勾)
[ ] state 由框架/实现校验,一次性作废
[ ] redirect 无通配,IdP 后台只有精确 URI
[ ] 仓库搜 SECRET 无真实值,secret 走环境变量
[ ] 前端无 NEXT_PUBLIC_ 类机密
[ ] 日志无完整 token(已打码)
[ ] 联邦用户默认最低权限 USER
步骤 5:(可选)挂上 Resource Server
# 中文注释:开放 API 前缀独立成链(完整配置见第 8 课)
spring.security.oauth2.resourceserver.jwt.issuer-uri: https://你的AS
# 中文注释:用 IdP 发出的、含 tracks:read 的 access 调开放接口
curl -i http://localhost:8080/api/v1/tracks \
-H "Authorization: Bearer OAUTH_ACCESS_JWT"
# 缺 scope → 403;无 token → 401
curl -i -X POST http://localhost:8080/api/v1/tracks \
-H "Authorization: Bearer READ_ONLY_TOKEN" \
-H "Content-Type: application/json" -d '{"slug":"demo"}'
步骤 6:回归密码轨(必做)
# 中文注释:OAuth 配好后,密码登录必须仍然可用——双轨并存的生命线
curl -i -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"你的密码"}'
curl -i http://localhost:8080/admin/tracks \
-H "Authorization: Bearer <返回的本站JWT>"
再验证一次「关掉 OAuth」(注释掉 registration 或用配置开关禁用)后密码轨不受影响。
步骤 7:写一段团队 README
何时用哪种 token:
站内会话(platform/admin)→ 本站自研 JWT,/auth/login 或联邦登录后签发
调 GitHub/Google API → IdP 的 access_token,仅后端持有
第三方调我们的开放 API → IdP 签发的 OAuth access(scope 限定),走 RS 链
三者互不通用;排错先看请求路径确定凭证种类
讲清楚要点
- 闭环跨三个系统(前端、后端、IdP),排错按箭头逐步看 Network,不要只盯后端日志——callback 没到、state 失效、换票 500、前端丢参,症状全不同。
- 本站业务授权在自家体系里:联邦成功必须映射用户并签发本站 JWT,而不是让前端拿着 GitHub 的 token 调 admin。
- Resource Server 是另一个角色:可选、独立成链、路径分区;别和 oauth2Login 拧成一个「万能开关」却说不清哪条链管哪些路径。
- 安全清单没勾完等于练习没完成:能演示只是及格线,secret/redirect/日志项是上线门禁。
- 双轨并存的关键是回归:每次改 Security 配置都重跑一遍密码登录 + 受保护接口,防「新轨上线、旧轨阵亡」。
- 文档留给未来的自己:三种 token 的适用路径写清楚,三个月后接手的人(或你自己)会感谢现在的你。
你可能会懵的地方
-
callback 到了 Spring 却 500?
顺序排查:secret 环境变量注入了吗;服务器时间准吗(state/码过期误判);scope 对吗;SuccessHandler 里有没有空指针(GitHub 邮件可能为 null)。建议先摘掉自定义 handler 跑通框架默认流程,再挂回钩子。
-
前端 callback 页拿不到 token?
三类原因:redirect 时没做 URL 编码(token 含 +/= 被截断);localhost 与 127.0.0.1 混用导致 cookie 域不一致;callback 页与 API 跨域时没带 credentials。逐项对齐域名与编码。
-
GitHub 第二次登录不弹同意页?
正常——已授权应用直接静默发 code,体验更好。想重新看到同意页:GitHub Settings → Applications 撤销授权,再走一遍。
-
加了 Resource Server 后 /admin 全 401?
RS 链的 securityMatcher 误伤(写成了 /**),或链的 @Order 排错把 /admin/** 抢先匹配。改回 /api/v1/** 并核对链顺序(第 8 课卡点急救同款)。
-
练习做到一半想直接上线?
先补三件事:生产 HTTPS 域名与生产专用 OAuth App、日志脱敏核查、密码轨回归。三项没做之前,只在本地演示,不部署。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 只做后端跳转,不接 platform 会话 | 联邦登录没有落地成可用状态 | 完成「映射用户 → 发 JWT → 前端 cookie」闭环 |
| 验收只看「能打开 GitHub 页」 | 只完成了流程前半段 | 以「本地已登录态 + 受保护接口 200」为准 |
| secret 写进仓库「作业方便」 | 机密泄露,练习反成反面教材 | 环境变量 + .env.example + gitignore |
| RS 与 admin 链混在一个过滤器里 | 两套凭证互相干扰,401 成谜 | securityMatcher 分区 + 注明链序 |
| 不做密码轨回归 | 新轨上线踩死旧轨 | 每次改 Security 都跑 admin 登录用例 |
卡点急救
- 现象:完整走完 GitHub 同意后,后端日志报
invalid_grant 或 code 相关错误。最常见是 code 被用了两次:前端刷新了 callback 页、或同时开了两个标签页共享 session。重新发起登录即可;开发期避免刷新 callback URL 本身。
- 现象:联邦登录成功,前端 URL 里能看到 token,但 cookie 没写上。检查 callback 页写 cookie 的 domain/path 与后续请求是否一致;确认用 HttpOnly cookie 的方案时写入动作发生在同源请求里(BFF/服务端写),而不是前端 document.cookie。
- 现象:想在演示里展示「缺 scope 的 403」,但总是 401。你拿的可能是自研 JWT 在打 RS 链,或 IdP 签的 token 根本没带 tracks:read。先确认凭证种类(issuer 是 IdP),再检查 IdP 侧 scope 配置是否真的发放了该 scope。
面试常问
-
描述你做过的 OAuth 登录闭环(面试复述模板)?
按四角色讲:前端按钮跳 /oauth2/authorization/github(框架生成 state 并 302);用户在 IdP 同意后回 callback;框架校验 state、用 code+secret 换 token、拉用户信息;我的 SuccessHandler 按 provider+externalId findOrCreate 本地用户,签发站内 JWT 回跳前端写 HttpOnly cookie;后续与密码登录同轨。补一句安全设计:secret 走环境变量、redirect 精确白名单、日志打码。
-
如果让你给这个系统加开放 API,怎么演进?
加 spring-boot-starter-oauth2-resource-server,/api/v1/** 独立成链,issuer-uri 指向授权服务器;scope 设计(tracks:read/write)+ @PreAuthorize 控制;开放凭证与站内会话严格分区;文档化 scope 契约。体现「角色分明、路径分区、凭证不混用」的架构意识。
-
上线前你怎么证明 OAuth 部分是安全的?
拿清单说话:state 一次性校验、PKCE(若含公共客户端)、redirect 精确匹配、secret 扫描通过、日志脱敏、refresh 轮换与撤销演练、密码轨回归通过。安全靠可复现的检查动作,不靠口头承诺。
自测三问
- 用五步描述你的最小闭环(从按钮点击到本站会话建立)。
- 三种 token(本站 JWT、IdP access、refresh)各应用在哪类路径?
- 上线前安全清单,你如何证明「做过」而不只是「知道」?
小结一句话
OAuth 实验闭环 = IdP 认人 + Spring Client + 本地用户映射 + 本站 JWT 会话(可选 Resource Server 开放分支);双轨回归 + 安全清单全勾,才算真正学完。
延伸
- Spring Security:OAuth2 Client
- oauth.net:OAuth 2.0
- 下一步方向:把本 track 的能力继续往外推——① 自建授权服务器(Spring Authorization Server,把「发票方」也掌握在自己手里);② 企业 SSO 与 OIDC 多租户(对接公司 IdP);③ 开放平台化(开发者后台、scope 申请审批、文档门户)。建议从①开始:Spring Authorization Server 官方快速上手文档是最好的下一段路。