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

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

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

学习

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

更多

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

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
OAuth 2.0 与 OpenID Connect/课时/Spring OAuth2 Client
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综合练习

Spring OAuth2 Client

spring.security.oauth2.client 配置。

40 分钟

Spring OAuth2 Client

这一课你将能做到

  1. 引入 spring-boot-starter-oauth2-client 并完成 GitHub 的 registration 配置(client-id/secret 走环境变量),跑通默认路径 /oauth2/authorization/github → /login/oauth2/code/github
  2. 写出 SecurityFilterChain 中 oauth2Login 的配置,说清框架替你做了哪些事:state 生成校验、code 交换、UserInfo 拉取
  3. 实现 OAuth2LoginSuccessHandler:映射/创建本地用户 → backend 签发本站 JWT → 302 回前端 callback,联邦登录与密码登录双轨并存

为什么要学

手写授权码流五步并不难,难的是每个细节都不出错:state 的生成与会话绑定、code 的一次性、redirect_uri 字节级一致、token 刷新、用户信息解析……Spring Security 6 的 OAuth2 Client 把这些全部收进框架,你只做两件事:填配置、写「登录成功之后」的业务钩子。

本站的角色定位要摆正:backend 对 GitHub 而言是 OAuth Client(去别人那登录);对自家前端而言,仍是那个「校验密码、签发自研 JWT」的认证服务。联邦登录成功后,不是把 GitHub 的 token 洒给前端,而是走和密码登录完全相同的收尾——查/建用户、发本站 JWT。这样前端 AuthProvider、后端鉴权过滤器一条都不用改。

面试角度:Security 6 的 lambda DSL、oauth2Login 与资源服务器/自研过滤器的共存、AuthenticationSuccessHandler 的作用时机,都是实战高频题。

先建立图像

用户点「GitHub 登录」(前端按钮只做跳转)
  → GET /oauth2/authorization/github        ← Spring 授权入口(自动生成 302)
  → 用户在 GitHub 登录并同意
  → GET /login/oauth2/code/github?code&state ← Spring 回调端点(自动校验+换票)
  → Spring 拉取 UserInfo,组装 OAuth2User
  → 你的 OAuth2LoginSuccessHandler:
      findOrCreate 本地用户 → jwtUtil.sign → 302 回前端 /login/callback?token=...
  → 前端写 cookie → 后续请求带本站 JWT → 与密码登录完全同轨
概念人话
registration「我作为哪个 Client 登记在 IdP」:id/secret/scope/redirect
providerIdP 的 authorize/token/userinfo 端点在哪(GitHub 内置免配)
OAuth2User框架从 IdP 拉回的用户属性 Map
SuccessHandler框架换完票、拿到用户之后,交给你处理的钩子

跟着做

步骤 1:注册 GitHub OAuth App

GitHub → Settings → Developer settings → OAuth Apps → New OAuth App
Homepage URL:      http://localhost:3000
Authorization callback URL:
  http://localhost:8080/login/oauth2/code/github   ← 与 Spring 默认路径一致

记下 Client ID,生成 Client Secret——两个值都放环境变量,绝不提交 git。

步骤 2:加依赖

<!-- 中文注释:Client 负责「去别人那登录」;与 Resource Server 是两个不同 starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

步骤 3:写 application.yml

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: ${GITHUB_CLIENT_ID}
            client-secret: ${GITHUB_CLIENT_SECRET}
            scope: read:user,user:email
            # redirect-uri 不写即用默认模板:{baseUrl}/login/oauth2/code/{registrationId}

GitHub 的 authorize/token/userinfo 端点是框架内置的 CommonOAuth2Provider,不需要配 provider 段。

步骤 4:SecurityFilterChain(Spring Security 6 语法)

package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class OAuth2ClientSecurityConfig {

    @Bean
    public SecurityFilterChain oauth2Chain(HttpSecurity http,
                                           OAuth2LoginSuccessHandler successHandler) throws Exception {
        http
            .securityMatcher("/oauth2/**", "/login/**")
            .authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
            .oauth2Login(oauth -> oauth
                .successHandler(successHandler)
                .failureHandler((req, res, ex) ->
                    res.sendRedirect("/login?error=" + ex.getClass().getSimpleName()))
            );
        return http.build();
    }
}

要点:用 securityMatcher 把 OAuth 登录链与自研 JWT 鉴权链分区——/admin/** 等业务接口继续走你现有的 JWT 过滤器链,互不干扰。

步骤 5:成功处理器——联邦登录的「收尾动作」

package com.example.auth;

import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;

import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import jakarta.servlet.http.HttpServletRequest;   // Spring Boot 3:jakarta 命名空间
import jakarta.servlet.http.HttpServletResponse;

@Component
public class OAuth2LoginSuccessHandler implements AuthenticationSuccessHandler {

    private final UserService userService;
    private final JwtUtil jwtUtil;
    private final String frontendUrl = "http://localhost:3000";

    public OAuth2LoginSuccessHandler(UserService userService, JwtUtil jwtUtil) {
        this.userService = userService;
        this.jwtUtil = jwtUtil;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException {
        OAuth2User oauthUser = (OAuth2User) authentication.getPrincipal();
        String externalId = oauthUser.getName(); // GitHub 默认把数字 id 放进 name,稳定
        Map<String, Object> attrs = oauthUser.getAttributes();

        // ① 映射/创建本地用户(provider + externalId 联合唯一)
        User user = userService.findOrCreateFromOAuth("github", externalId, attrs);

        // ② 签发本站自研 JWT——与密码登录同一把钥匙
        String token = jwtUtil.sign(user.getId(), user.getRole());

        // ③ 302 回前端 callback,前端写入与密码登录相同的会话
        String redirect = frontendUrl + "/login/callback?token="
                + URLEncoder.encode(token, StandardCharsets.UTF_8);
        response.sendRedirect(redirect);
    }
}

步骤 6:前端只加一个入口按钮

Next 「GitHub 登录」按钮
  → window.location.href = `${API_BASE}/oauth2/authorization/github`
  → 授权完成后落到 /login/callback?token=...
  → AuthProvider 写入与密码登录相同的 cookie

前端全程不接触 GitHub 的 code/token,也不需要知道 client_secret。

步骤 7:验收与密码轨回归

# 中文注释:OAuth 配好后,密码登录必须仍然可用(双轨并存)
curl -i -X POST http://localhost:8080/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"你的密码"}'

# 带 JWT 访问受保护接口
curl -i http://localhost:8080/admin/tracks \
  -H "Authorization: Bearer <本站JWT>"

浏览器完整走一遍 GitHub 登录:应最终带着本站 JWT 回到 platform 并显示已登录。

讲清楚要点

  1. Spring OAuth2 Client 实现的是 Client 角色:它帮你跳转、换票、拉用户,但不会把你的业务 API 变成 Resource Server——那是另一个 starter、另一条链(下一课)。
  2. state、code 交换、redirect 校验由框架代劳:这正是选框架的理由;你手写的版本十有八九会漏 state 一次性作废之类的细节。
  3. secret 只属于机密客户端,必须来自环境变量/密钥管理,禁止写进 application.yml 提交仓库。
  4. 回调路径约定为 /login/oauth2/code/{registrationId}:IdP 控制台的 Callback URL 必须与之逐字一致。
  5. IdP 用户不是本站用户:成功处理器必须做 findOrCreate,主键用 (provider, externalId),别用可变的 login/邮箱。
  6. 联邦成功后发本站 JWT:前端会话、后端鉴权、权限模型全部复用密码登录的既有轨道——这是本站双轨并存的架构关键。

你可能会懵的地方

  • oauth2Login 和 oauth2Client 是一回事吗? oauth2Login() 用于登录场景(拿用户身份建本地会话);另有 oauth2Client() 用于「调用外部 OAuth API 时自动携带/刷新 token」。本课聚焦登录。

  • 为什么本地 http 可以,生产必须 https? 多数 IdP 生产环境要求 HTTPS 的 redirect_uri;localhost 通常被例外放行。上生产前把 callback 换成 https 域名并在 IdP 登记生产地址(建议本地/生产分开两个 OAuth App)。

  • GitHub 返回的属性里没有 email? 需要申请 user:email scope,且部分用户主邮箱未公开,可能要额外调 /user/emails。主键仍用数字 id,email 只作资料并做好空值兜底。

  • 和自研 JWT 过滤器冲突吗? 不冲突,前提是分区:OAuth 链只 match /oauth2/**、/login/**;业务接口走原 JWT 链。成功处理器发完本站 JWT 后,一切回归原轨道。

  • 想拿 GitHub 的 access_token 去调 GitHub API 怎么办? 在成功处理器里从 OAuth2AuthorizedClientRepository 取授权客户端,或用 OAuth2AuthorizedClientManager;注意该 token 只用于调 GitHub,与本站会话无关。

错法对照

错法问题正解
secret 写进 application.yml 提交仓库泄露即身份被冒用${ENV} + 本地/CI 注入
前端拿 client_secret 直接调 GitHub机密进了浏览器只有 Java 端换票
成功后不建本地用户,每次直接透传 GitHub 身份站内权限/资料无处安放findOrCreate + 发本站 JWT
callback 写成前端地址却让 Spring 接 codecode 到不了换票端点redirect 指向后端约定路径
关掉密码登录「只留 GitHub」IdP 故障即全站锁死双轨并存,admin 保留密码应急

卡点急救

  1. 现象:redirect_uri_mismatch。GitHub 后台的 Callback URL 必须与 Spring 实际回调路径逐字一致:http://localhost:8080/login/oauth2/code/github。检查端口、斜杠、registrationId 拼写(不是 clientId)。
  2. 现象:回调后 Whitelabel 404 或 authorization_request_not_found。多见于 session 丢失(跨域 cookie、负载均衡会话不粘)或重启后旧 state 失效。开发期清浏览器 cookie 重试;生产确认 session/Redis 会话共享正常。
  3. 现象:成功后前端拿到的 token 少了字符或解析失败。redirect 拼接时没做 URL 编码,JWT 里的 +/= 被吃掉。确认 URLEncoder.encode(token, StandardCharsets.UTF_8),前端取参后按对应方式解码。

面试常问

  • Spring Security 的 oauth2Login 帮你做了哪些事? 生成并校验 state(防 CSRF)、跳转 authorize、回调接 code、用 code+secret 换 token、调用 UserInfo 组装 OAuth2User,最后把 Authentication 交给你的 SuccessHandler。你只需要写成功后的业务(建号、发本站 JWT、跳转)。

  • 联邦登录为什么要桥接回自己的会话(发本站 JWT)? 站内权限模型在本地库;本站 JWT 短寿命可控可撤销;前端与后端鉴权轨道零改动。让 OAuth 只负责「认人」,会话与授权仍是自己的——职责分离。

  • SecurityMatcher 分区是什么?为什么要分? Spring Security 支持多条 SecurityFilterChain,按 securityMatcher 路由。OAuth 登录链与自研 JWT 鉴权链各管各的路径,避免一个过滤器「什么都想验」导致的 401 混乱——这是双轨并存项目的标准组织方式。

自测三问

  1. 写出从点击按钮到拿到本站 JWT 的完整路径序列。
  2. GitHub OAuth App 的 Callback URL 在默认配置下应填什么?
  3. 为什么联邦成功后要签发本站 JWT,而不是让前端长期持有 GitHub 的 access_token?

小结一句话

Spring OAuth2 Client = 配好 registration,框架跑授权码流,你在 SuccessHandler 里映射用户并签发本站 JWT——联邦登录从此只剩「成功之后」的业务。

延伸

  • Spring Security:OAuth2 Client
  • Spring Boot:Common Application Properties(security 部分)
  • 下一课:Resource Server——反过来,让你的 API 验「别人拿来的」OAuth access_token。
加载本节练习…
← 上一课Refresh Token下一课 →Resource Server