一、什么是OAuth2

(1)概念

OAuth 2.0 是一个授权框架,允许第三方应用有限地访问用户在另一服务上存储的资源(如照片、联系人、文件),而无需将用户名和密码直接交给第三方应用

解决了一个问题:如何让用户在不把密码告诉第三方应用的前提下,授权第三方访问自己在服务提供商上的受保护资源?

(2)四类角色

在OAuth 2.0中,有四类角色,它们分别为

  • 资源所有者(Resource Owner):用户本人
  • 客户端(Client):想访问资源的第三方应用
  • 授权服务器(Authorization Server):负责验证用户并颁发令牌
  • 资源服务器(Resource Server):存放用户资源的地方(如照片服务器)

注意:很多企业内部系统中,授权服务器和资源服务器往往是同一台服务器。

举个例子,假如我开发了一个 Spring Boot Web 应用,用户可以在你的应用上点击「使用 GitHub 登录」,你的应用需要读取用户在 GitHub 上的部分信息(比如邮箱、用户名)。那么在这种情况下,角色关系可以这样对照:

角色 对应 说明
资源所有者 你的用户(用 GitHub 登录的那个人) 他的 GitHub 账号数据属于他自己
资源服务器 GitHub API 服务器(如 api.github.com 存放用户邮箱、仓库、组织等数据的地方
授权服务器 GitHub OAuth 认证服务器(如 github.com/login/oauth/access_token 负责让用户登录、询问用户是否同意授权、颁发令牌
客户端 你的 Spring Boot 应用 它需要请求资源(用户数据),但拿不到用户密码,只能靠令牌

(3)基本工作流程

OAuth2.0工作流程可用下图说明

image.png

(4)授权码模式

OAuth 2.0定义了 4 种授权方式,分别为

  • 授权码模式:最安全、最常用的模式,专门用于有后端服务器的Web应用。
    • 授权码是一次性的,有效期很短(通常几分钟)
    • 必须配合 Client Secret(客户端密钥)才能换令牌
    • 令牌不经过前端,避免被浏览器插件、历史记录等拦截
    • 支持刷新令牌,可以自动获取新访问令牌
  • 隐式模式:已废弃,为纯前端应用设计,但存在安全缺陷
    • 没有授权码环节,直接返回令牌
    • 令牌出现在浏览器URL中(#access_token=xxx
    • 无法使用刷新令牌(但可以用iframe技巧延长,但非常麻烦)
  • 密码模式:用户直接把用户名+密码交给客户端,客户端用它们去换令牌。
    • 最简单直接,只需一次请求
    • 客户端直接接触到用户密码
    • 支持刷新令牌
  • 客户端凭证模式:客户端以自己的名义(而不是用户的名义)访问资源
    • 不涉及任何用户,没有“授权”环节
    • 令牌代表客户端本身的身份
    • 只能访问客户端自己的资源,不能访问用户数据

image.png

总结如下

模式 用户参与 获取令牌方式 刷新令牌 安全性 推荐程度
授权码模式 需要 授权码→令牌 ✅ 支持 ⭐⭐⭐⭐⭐ 最推荐
隐式模式 需要 直接返回 ❌ 不支持 ⭐⭐ 已废弃
密码模式 需要(交密码) 直接返回 ✅ 支持 ⭐⭐⭐ 仅特定场景
客户端模式 不需要 直接返回 ✅ 支持 ⭐⭐⭐⭐ 服务间调用

二、Github OAuth App案例分析

接下来,我们通过一个简单的dashboard应用,然后借助Github OAuth来展示这一过程。如下动图所示

PixPin_2026-06-06_17-04-57.gif

(1)准备工作

A:GitHub OAuth2 App 注册

首先,要在 GitHub 注册 OAuth2 App,步骤如下

  • 登录 GitHub → 右上角头像 → Settings
  • 左侧菜单最底部 → Developer settingsOAuth AppsNew OAuth App
  • 填写表单:
    • Application name:表示应用名称,可填任意。这里我填oauth2-demo
    • Homepage URL:表示应用首页地址。这里我填http://localhost:8080
    • Application description:描述信息
    • Authorization callback URL:回调地址。当授权服务器在用户同意授权后,会把授权码或令牌发送到这个地址。必须填写正确,按照Github OAuth2约定,需要填http://localhost:8080/login/oauth2/code/github
  • 点击 Register application
  • 生成 Client Secret(点击 “Generate a new client secret”)
  • Client IDClient Secret 填入 application.yml

image.png
image.png
image.png


注意:Spring Security OAuth2 Client 的回调地址是约定优于配置的体现:

/login/oauth2/code/{registrationId}  
                        ↑              
            application.yml 中的 key:github  
  • /login/oauth2/code/ 是固定前缀,由 OAuth2LoginAuthenticationFilter 拦截
  • {registrationId} 是你配置的注册 ID(即 spring.security.oauth2.client.registration.github 中的 github

如果你配置了多个 OAuth2 提供方(比如同时用 GitHub 和 Google),就分别有:

  • /login/oauth2/code/github
  • /login/oauth2/code/google

B:项目环境及启动应用

项目结构

oauth2-demo/  
├── pom.xml                                      # Maven 依赖配置  
├── src/main/java/com/example/oauth2/  
│   ├── OAuth2DemoApplication.java               # Spring Boot 启动类  
│   ├── config/  
│   │   └── SecurityConfig.java                  # Spring Security + OAuth2 核心配置  
│   └── controller/  
│       └── HomeController.java                  # 页面控制器,获取 OAuth2 用户信息  
└── src/main/resources/  
    ├── application.yml                          # OAuth2 客户端注册信息  
    └── templates/
        ├── index.html                           # 公开首页
        └── dashboard.html                       # 登录后仪表盘(显示 GitHub 用户数据)  

技术栈

组件 版本 作用
Spring Boot 3.3.5 应用框架
Spring Security 6.3.4 安全框架
spring-boot-starter-oauth2-client 3.3.5 OAuth2 客户端自动配置
Thymeleaf 3.1.2 服务端模板引擎
Java 21 运行环境

启动应用

mvn spring-boot:run

(2)工作流程

A:概述

上面动图所展示流程,用语言描述如下

  1. 用户在浏览器访问 GET /dashboard,请求到达 Spring Boot 应用。
  2. Spring Security 检测到用户未登录,触发重定向,返回 302 /oauth2/authorization/github
  3. 浏览器跟随重定向,发送 GET /oauth2/authorization/github
  4. Spring Security 生成 OAuth2 授权请求,返回 302 重定向到 GitHub 授权页。
  5. 浏览器跳转到 GitHub 的授权端点:GET https://github.com/login/oauth/authorize?response_type=code&client_id=Ov23li...&redirect_uri=.../code/github&scope=user:email&state=随机字符串
  6. GitHub 返回授权页面,显示 "oauth2-demo wants to access your account",提示用户确认授权。
  7. 用户在 GitHub 授权页面上点击 "Authorize"
  8. GitHub 处理授权同意后,302 回调我们的应用,地址为 /login/oauth2/code/github?code=xxx&state=xxx
  9. 浏览器发送 GET /login/oauth2/code/github?code=xxx&state=xxx 到我们的应用。
  10. Spring Security 拦截该请求,首先校验 state 参数是否与之前存入 Session 的值一致,防止 CSRF 攻击。
  11. 校验通过后,Spring Security 在后端发起 POST /login/oauth/access_token 请求到 GitHub,携带 client_idclient_secretcode。GitHub 返回 access_token。此过程浏览器不可见。
  12. Spring Security 拿到 access_token 后,在后端发起 GET https://api.github.com/user,请求头携带 Authorization: Bearer access_token。GitHub 返回用户信息 JSON(loginnameavatar_url 等)。
  13. Spring Security 将用户信息封装为 OAuth2User 对象,创建 OAuth2AuthenticationToken,并存入 HTTP Session。
  14. 认证完成,Spring Security 返回 302 重定向到原始请求路径 /dashboard
  15. 浏览器发送 GET /dashboard,请求头携带 Cookie: JSESSIONID=xxx
  16. 应用从 HTTP Session 中取出 OAuth2User,渲染 dashboard 页面并返回 200 OK + HTML

image.png


上述过程就体现出了OAuth2中的一些概念和参数,我们再回顾和了解一下

概念 说明 代码体现
Client ID 应用的公开标识,类似"用户名" Ov23li5Jjk9k4Fr6M8QK
Client Secret 应用的密钥,类似"密码",绝对不能泄露到前端 ae6ee87...(服务端持有)
Redirect URI 授权成功后跳回的地址,必须与注册时完全一致 /login/oauth2/code/github
Scope 申请的权限范围(只读/读写/某子集) user:email
State 随机字符串,防 CSRF 攻击 Spring Security 自动生成并校验
Authorization Code 一次性授权码,短时效(通常 10 分钟) GitHub 回调 URL 中的 code 参数
Access Token 访问令牌,用 code 换来的凭证 后端持有,前端不可见
Authorization Server 颁发令牌的服务器 https://github.com/login/oauth/authorize
Token Endpoint 用 code 换 access_token 的端点 https://github.com/login/oauth/access_token
UserInfo Endpoint 获取用户信息的端点 https://api.github.com/user

B:另一个视角

接着,我们通过控制台网络请求日志的方式,来再次感受一下这个过程。如下,打开控制,勾选保留日志,然后依次进行授权操作

{D11F191D-077E-4B0F-929D-DC7AED078207}.png

从这些请求中,就可以看到如下几个重要的请求

1、/dashboard302 — 请求到达应用,Spring Security 发现 SecurityContext 为空,未认证,拒绝放行。

{590DAD48-6AEB-4A2D-9923-B6D00A3E5CDD}.png
2、/oauth2/authorization/github302OAuth2AuthorizationRequestRedirectFilter 生成 state 存 Session,拼接完整授权 URL,重定向到 GitHub。

{6689C620-0A72-4C8E-B151-5A198C81FC71}.png
3、github.com/login/oauth/authorize?client_id=...&redirect_uri=...&scope=user:email&state=...302 — GitHub 检测到用户未登录则先让用户登录,已登录则展示授权页;用户点"Authorize"后 GitHub 生成一次性 code,302 回调我们的应用。

{80098FB9-38EF-4737-86A5-CFFD3738DCF0}.png

4、/login/oauth2/code/github?code=xxx&state=xxx302OAuth2LoginAuthenticationFilter 拦截该请求,校验 state,后端用 codeaccess_token,再用 access_token 取用户信息,创建 OAuth2User 存入 Session,最后重定向回 /dashboard

{61CF3FB7-289C-4141-B362-D2D8BDABBE8E}.png

5、/dashboard200 — 此时请求携带 JSESSIONID cookie,SecurityContextPersistenceFilter 从 Session 恢复认证信息,通过授权,渲染页面返回。

  • 有时候会在后面加上 ?continue 参数,它是 RequestCache 在重建 URL 时附加的内部追踪参数,不影响功能。

{6F89A272-F1DE-430F-8138-4B11D1DC4B2A}.png

(3)代码解析

https://github.com/Zhangfen21082/oauth2

A:pom.xml

最重要的依赖是 spring-boot-starter-oauth2-client,它自动注册了:

  • OAuth2LoginAuthenticationFilter — 拦截 /login/oauth2/code/* 路径
  • OAuth2AuthorizationRequestRedirectFilter — 处理重定向到授权服务器
  • OAuth2UserService — 加载用户信息
<dependencies>
    <!-- Spring Web:提供 Tomcat + Spring MVC -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Security:提供认证授权框架 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <!-- OAuth2 Client -->
    <!-- 提供:
         - OAuth2LoginAuthenticationFilter:拦截回调 URL
         - DefaultAuthorizationCodeTokenResponseClient:用 code 换 token
         - OAuth2UserService:用 token 取用户信息
         - CommonOAuth2Provider:预置 GitHub/Google/Facebook 等提供商配置
    -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-client</artifactId>
    </dependency>

    <!-- Thymeleaf:服务端渲染 HTML 模板 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
</dependencies>

B:application.yml

①:OAuth2完整配置要素

对接一个 OAuth2 提供商,本质上是告诉 Spring Security 两件事:“我是谁”“我去哪”。完整的 YAML 配置结构分为 registrationprovider 两部分:

spring:
  security:
    oauth2:
      client:
        # ── 第一部分:registration,回答"我是谁" ──
        registration:
          my-provider:                    # registrationId,自定义名称,后续 URL 中会用到
            client-id: xxxxx              # ① 客户端标识,提供商标识"你是谁"
            client-secret: xxxxx          # ② 客户端密钥,相当于密码,绝不能泄露
            scope: openid,profile,email   # ③ 申请的权限范围,由提供商定义
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
                                          # ④ 回调地址,baseUrl 和 registrationId 会自动替换

        # ── 第二部分:provider,回答"我去哪" ──
        provider:
          my-provider:                    # ⑤ 名称必须与上面 registration 的 key 一致
            authorization-uri: https://provider.com/oauth/authorize
                                          # ⑥ 授权端点,用户点"授权"时跳转到这个地址
            token-uri: https://provider.com/oauth/token
                                          # ⑦ 令牌端点,后端用 code 换 access_token 的地址
            user-info-uri: https://provider.com/userinfo
                                          # ⑧ 用户信息端点,后端用 token 获取用户资料的地址
            user-name-attribute: sub       # ⑨ UserInfo 返回 JSON 中,哪个字段是用户唯一标识
②:OIDC Discovery 模式(更简化的写法)

如果你的 OAuth2 提供商实现了 OpenID Connect(OIDC) 协议,provider 部分只需要一行 issuer-uri,无需手动写三个端点地址:

  • 判断是否能用 issuer-uri:访问 https://你的提供商域名/.well-known/openid-configuration,如果返回 JSON 就是支持 OIDC。
spring:
  security:
    oauth2:
      client:
        registration:
          my-app:
            provider: cloudos-iam             # ← 引用 provider 配置
            client-id: ${CLIENT_ID}           # 从环境变量读取,安全
            client-secret: ${CLIENT_SECRET}
            scope: openid,profile,email
            redirect-uri: ${REDIRECT_URI}     # 从环境变量读取
            authorization-grant-type: authorization_code  # 可省略(默认值)
            client-name: my-app               # 显示名称,不影响流程
        provider:
          cloudos-iam:
            issuer-uri: https://iam.company.com   # ← 一行替代三个 URI

issuer-uri 的工作原理

Spring Security 启动时会自动请求 https://iam.company.com/.well-known/openid-configuration,该端点返回标准 JSON:

{
  "issuer": "https://iam.company.com",
  "authorization_endpoint": "https://iam.company.com/oauth/authorize",
  "token_endpoint": "https://iam.company.com/oauth/token",
  "userinfo_endpoint": "https://iam.company.com/userinfo",
  "jwks_uri": "https://iam.company.com/oauth/jwks",
  "scopes_supported": ["openid", "profile", "email"],
  "...": "..."
}

Spring Security 自动解析这个 JSON,把 authorization_endpointtoken_endpointuserinfo_endpoint 注入到运行时配置,等价于你手动写了三个 URI。同时 user-name-attribute 也不需要你指定,因为 OIDC 标准规定 sub 字段就是用户唯一标识。

这种写法的优势

  • 端点地址变更时(比如 IAM 系统迁移),不需要改应用配置,只要 IAM 的 .well-known/openid-configuration 返回新的地址即可
  • 配置更简洁,不容易写错地址
  • OIDC 兼容意味着各家 OIDC 提供商的对接方式完全一致
③:预置提供商 — 为什么 GitHub 只配两行

可以看到,对于本项目,application.yml 中的配置非常简单,只有:

spring:
  security:
    oauth2:
      client:
        registration:
          github:                      # ← registrationId,自定义名称
            client-id: xxxxxx
            client-secret: xxxxxx

这是因为,github 这个 registrationId 被 Spring Security 的 CommonOAuth2Provider 枚举匹配到了。Spring Security 预置了 GitHub、Google、Facebook、Okta 四个常用提供商的参数:

// spring-security-oauth2-client 源码
public enum CommonOAuth2Provider {
    GITHUB {
        @Override
        public Builder getBuilder(String registrationId) {
            return new Builder()
                .registrationId(registrationId)
                .authorizationUri("https://github.com/login/oauth/authorize")
                .tokenUri("https://github.com/login/oauth/access_token")
                .userInfoUri("https://api.github.com/user")
                .userNameAttributeName("id");
        }
    },
    GOOGLE { /* ... */ },
    FACEBOOK { /* ... */ },
    OKTA { /* ... */ }
}

对于这三种类型,总结如下

image.png

C:SecurityConfig.java

package com.example.oauth2.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.config.Customizer;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // ① 定义访问控制规则
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/error").permitAll()
                .anyRequest().authenticated()
            )
            // ② 启用 OAuth2 登录
            .oauth2Login(Customizer.withDefaults())
            // ③ 退出登录配置
            .logout(logout -> logout
                .logoutSuccessUrl("/")
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .deleteCookies("JSESSIONID")
            );
        return http.build();
    }
}

@EnableWebSecurity — 启用 Spring Security 的 Web 安全支持,会导入 HttpSecurityConfiguration 等自动配置。

authorizeHttpRequests(...) — 定义 HTTP 请求的访问控制:

  • .requestMatchers("/", "/error").permitAll()//error 任何人(包括未登录用户)都可访问
  • .anyRequest().authenticated() — 除上述两个路径外,所有请求都必须已认证
  • 当未登录用户访问 /dashboard 时,Spring Security 会自动触发 OAuth2 授权流程(重定向到 GitHub)

.logout(...) — 退出登录配置:

  • .logoutSuccessUrl("/") — 退出后跳转到首页
  • .invalidateHttpSession(true) — 失效 HTTP Session
  • .clearAuthentication(true) — 清除认证信息
  • .deleteCookies("JSESSIONID") — 删除 Session Cookie

三、SSO 简介

(1)概念

SSO(Single Sign-On,单点登录)解决的是这样一个问题:

公司内部有多个系统(OA、Wiki、Jenkins、GitLab……),每个都要求登录。用户能不能只登录一次,就能访问所有这些系统?

答案就是 SSO。它和 OAuth2 的关系如下:

OAuth2 SSO
本质 授权协议 一种使用场景
核心问题 “怎么让第三方应用安全地访问用户资源” “怎么让用户一次登录,处处可用”
实现方式 通常是 OAuth2 + OIDC(部分场景也用 SAML)
关系 基础设施 上层目标

简单说:SSO 是目标,OAuth2 + OIDC 是当前主流实现手段。

(2)扩展

把场景扩展为两个应用(A 和 B),两个应用各自注册了不同的 Client ID / Client Secret,但都对接同一个 GitHub:

image.png

需要注意

  • A 和 B 各自有独立的 Client ID 和 Client Secret,各自走完整的 OAuth2 授权码流程(各自拿到各自的 code,各自用自己的 secret 换各自的 token)
  • B 没有"直接拿令牌",它同样经历:跳转 → code → 换 token → 取用户 → 建 Session,一步没少
  • SSO 省掉的只有两样:输入密码(因为 GitHub 已登录)和部署这套相同的流程
  • “Authorize” 这一步,每个新应用首次访问都要点一次。点过之后,GitHub 记入授权列表,下次再访问该应用才跳过

(3)SSO 的核心前提

前提 说明
同一个授权服务器 所有应用都接同一个 SSO 服务器(如公司 IAM 系统)
用户在该服务器上已登录 浏览器携带了 SSO 服务器的 Session Cookie
每个应用独立注册 A 和 B 各自有独立的 Client ID / Secret,各自独立完成 OAuth2 流程

三条缺一不可。如果用户在 GitHub 上主动退出了(Revoke 或 Sign out),重新访问任意应用都会再次要求输入密码(登录这一步要从头来)。如果某个应用从未被该用户授权过,首次访问仍会弹"Authorize"。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐