什么是OAuth2?
核心思想:一个绝妙的比喻
想象一下这个场景:
你住进了一家酒店,有一张房卡。现在,你想让酒店的保洁服务在你外出时帮你打扫房间,但你肯定不想把你的房卡直接给保洁员。因为这张房卡不仅能开你的房门,可能还能进入健身房、餐厅消费等。你更不希望保洁员拿着你的卡去复制一张。
于是,你找到了酒店前台,对他们说:“请在下午2点到3点之间,授权保洁服务进入我的房间(仅限打扫)。”
前台验证了你的身份后,给了保洁服务一张临时的、只能开你房门、并且只在指定时间段有效的“工作卡”。保洁员用这张卡完成了工作,之后这张卡就失效了。他们自始至终没有接触到你那张功能强大的主房卡。
这个比喻完美地解释了 OAuth 2.0 的核心:
- 你 (You):资源所有者 (Resource Owner),数据(房间)的拥有者。
- 你的房卡 (Your Key Card):你的用户名和密码,最高权限凭证。
- 保洁服务 (Cleaning Service):客户端应用 (Client Application),一个想要访问你部分数据的第三方应用。
- 酒店前台 (Hotel Front Desk):授权服务器 (Authorization Server),负责验证你的身份,并根据你的许可,发放“临时工作卡”。
- 你的房间 (Your Room):资源服务器 (Resource Server),存放着你的数据(例如,你的Google相册、微信好友列表)。
- 临时工作卡 (Temporary Work Card):访问令牌 (Access Token),一个临时的、有特定权限(Scope)、有有效期的凭证。
OAuth 2.0 的本质就是“授权委托” (Delegated Authorization)。 它允许用户授权第三方应用访问他们存储在另外一个服务上的数据,但无需将自己的用户名和密码提供给该第三方应用。
OAuth 2.0 的四个关键角色
-
资源所有者 (Resource Owner)
- 就是用户本人,是数据的所有者。
-
客户端 (Client)
- 想要访问用户数据的第三方应用程序。例如,一个想要读取你Google相册来打印照片的网站。
-
授权服务器 (Authorization Server)
- 整个流程的核心,负责:
- 验证资源所有者(用户)的身份。
- 获取资源所有者的授权(“你是否同意XX应用访问你的头像和昵称?”)。
- 如果授权成功,向客户端发放访问令牌 (Access Token)。
- 例如:
accounts.google.com
,graph.facebook.com
。
- 整个流程的核心,负责:
-
资源服务器 (Resource Server)
- 存储受保护资源的服务器。它会验证客户端出示的访问令牌,如果令牌有效且权限足够,就向客户端提供其请求的数据。
- 例如:Google Photos API, GitHub API。
注意:在很多大型服务中(如Google、Facebook),授权服务器和资源服务器虽然是两个逻辑角色,但可能由同一家公司运营,甚至部署在同一组服务器上。
核心组件:令牌 (Tokens)
令牌是 OAuth 2.0 流程的“通行证”。
-
访问令牌 (Access Token)
- 作用:客户端携带它去访问资源服务器,以获取数据。
- 特点:
- 生命周期短:通常只有几分钟到几小时,以降低泄露风险。
- 权限有限 (Scoped):令牌包含的权限范围(Scope)由用户授权决定。例如,一个令牌可能只能“读取头像”,不能“修改资料”。
- 格式:通常是一个不透明的字符串,或者是一个 JWT (JSON Web Token)。JWT 的好处是它自身就包含了权限、过期时间等信息,资源服务器可以自行验证,而无需再去查询授权服务器。
-
刷新令牌 (Refresh Token)
- 作用:当访问令牌过期后,客户端可以使用刷新令牌向授权服务器申请一个新的访问令牌,而无需用户再次登录授权。
- 特点:
- 生命周期长:可以是几天、几个月,甚至永久有效(除非用户撤销)。
- 高度敏感:必须安全地存储在客户端。一旦泄露,攻击者就可以不断获取新的访问令牌。
- 一次性或可重复使用:取决于授权服务器的策略。
-
授权码 (Authorization Code)
- 这是一个临时的、一次性的中间凭证。它的存在是为了安全。客户端先用它换取访问令牌和刷新令牌,这个交换过程在安全的后端服务器之间进行,避免了敏感的令牌在不安全的浏览器环境(前端)中传递。
四种主要的授权流程 (Grant Types)
OAuth 2.0 是一个“框架”,它定义了多种获取访问令牌的方式,以适应不同的应用场景。
1. 授权码模式 (Authorization Code Grant) - 最常用、最安全
这是功能最完整、流程最严密的授权模式。
-
适用场景:有后端服务器的 Web 应用(例如,一个Java/Python/Node.js网站)。
-
流程图解:
- 用户请求授权:用户在客户端应用上点击“使用微信登录”。客户端将用户重定向到微信的授权服务器,并附带参数:
client_id
(客户端ID),redirect_uri
(回调地址),scope
(申请的权限),response_type=code
。 - 用户授权:用户在微信的页面上登录,并同意授权。
- 返回授权码:微信的授权服务器将用户重定向回客户端预设的
redirect_uri
,并附上一个授权码 (code)。 - 换取令牌:客户端的后端服务器收到授权码后,带上这个
code
、client_id
和client_secret
(客户端密钥),向微信的授权服务器发起请求,换取 Access Token 和 Refresh Token。 - 访问资源:客户端后端使用 Access Token 向微信的资源服务器请求用户信息。
- 用户请求授权:用户在客户端应用上点击“使用微信登录”。客户端将用户重定向到微信的授权服务器,并附带参数:
-
优点:Access Token 和 Refresh Token 不会经过用户浏览器,只在服务器之间传递,非常安全。
2. 授权码模式 + PKCE (Proof Key for Code Exchange)
这是对授权码模式的增强,现在是移动应用和单页应用 (SPA) 的最佳实践。
- 解决的问题:在移动端等“公共客户端”中,无法安全地存储
client_secret
。如果有恶意应用在手机上拦截了上一步中的授权码code
,它就可以冒充正常应用去换取令牌。 - PKCE 流程:
- (发起请求前)客户端生成一个随机字符串
code_verifier
,并对其进行哈希运算得到code_challenge
。 - (请求授权时)客户端将
code_challenge
和哈希算法一起发送给授权服务器。 - (换取令牌时)客户端在发送
code
的同时,也发送原始的code_verifier
。 - 授权服务器收到后,用同样的哈希算法计算
code_verifier
,与之前收到的code_challenge
进行比对。如果一致,才发放令牌。
- (发起请求前)客户端生成一个随机字符串
- 优点:即使
code
被截获,由于攻击者没有code_verifier
,也无法换取令牌,有效防止了授权码拦截攻击。
3. 简化模式 (Implicit Grant) - 已不推荐
- 适用场景:纯前端应用(没有后端服务器的 SPA)。
- 流程:用户授权后,授权服务器直接将 Access Token 作为 URL 的一部分返回给客户端。
- 缺点:
- 令牌直接暴露在浏览器中,不安全。
- 不支持 Refresh Token,令牌过期后用户必须重新授权。
- 现状:由于安全风险,已被 授权码+PKCE 模式取代。
4. 客户端凭证模式 (Client Credentials Grant)
- 适用场景:没有用户参与的场景,即应用以自己的名义访问资源,而不是代表某个用户。例如,一个后台服务需要调用另一个服务的API。
- 流程:客户端直接使用自己的
client_id
和client_secret
向授权服务器申请访问令牌。整个过程与用户无关。
5. 资源所有者密码凭证模式 (Resource Owner Password Credentials Grant) - 已不推荐
- 适用场景:用户高度信任客户端应用,例如官方自己开发的应用。
- 流程:客户端直接收集用户的用户名和密码,然后向授权服务器换取令牌。
- 缺点:完全违背了 OAuth 的初衷(不暴露密码),风险极高。除非万不得已,否则绝不使用。
OAuth 2.0 vs OpenID Connect (OIDC) - 一个常见的混淆点
这是一个非常重要的区别:
- OAuth 2.0 是一个授权 (Authorization) 框架。它解决的问题是“你能做什么?”(Can you access my photos?)。它的产物是 Access Token。
- OpenID Connect (OIDC) 是一个构建在 OAuth 2.0 之上的认证 (Authentication) 协议。它解决的问题是“你是谁?”(Are you John Doe?)。
当你看到“使用Google登录”时,这背后其实是 OIDC 在工作。流程是这样的:
- 整个流程和 OAuth 2.0 的授权码模式几乎一样。
- 但在请求时,
scope
参数里会包含一个特殊的值openid
。 - 最终,授权服务器除了返回 Access Token,还会返回一个 ID Token。
- 这个
ID Token
是一个 JWT,其中包含了用户的身份信息(如用户ID、姓名、邮箱等)。客户端可以通过验证这个 ID Token 来确认用户的身份。
简单说:OIDC = OAuth 2.0 + ID Token。 OIDC 负责登录认证,OAuth 2.0 负责后续的 API 访问授权。
总结
特性 | 描述 |
---|---|
核心目的 | 授权委托:让用户在不泄露密码的情况下,授权第三方应用访问其在另一服务上的资源。 |
关键角色 | 资源所有者、客户端、授权服务器、资源服务器。 |
核心组件 | Access Token (访问令牌)、Refresh Token (刷新令牌)。 |
最佳实践 | - 后端 Web 应用:使用授权码模式 (Authorization Code Grant)。 - 移动/单页应用 (SPA):使用授权码模式 + PKCE。 - M2M (机器对机器):使用客户端凭证模式 (Client Credentials Grant)。 |
安全基石 | - HTTPS/TLS:所有通信必须加密。 - 短生命周期的 Access Token。 - 权限范围控制 (Scope)。 - 安全的 redirect_uri 校验。 |
与 OIDC 的关系 | OAuth 2.0 用于授权(你能做什么),OIDC 基于 OAuth 2.0 用于认证(你是谁)。 |
OAuth 2.0 因其灵活性和安全性,已成为现代 Web 和移动应用 API 授权的事实标准。理解它的工作原理对于任何 Web 开发者来说都至关重要。
如何集成OAuth2?
好的,我们来深入探讨一下如何在 Spring Security 中集成 OAuth 2.0。这是一个非常常见的需求,Spring Security 提供了强大且模块化的支持。
首先,最重要的一点是:你的应用程序在 OAuth 2.0 流程中扮演什么角色?
Spring Security 将其支持分为三个主要场景,这与我们之前讨论的 OAuth 2.0 角色相对应:
- OAuth 2.0 客户端 (Client):你的应用需要使用第三方服务(如 Google、GitHub、微信)进行登录,或者需要调用这些服务的 API。这是最常见的用例。
- OAuth 2.0 资源服务器 (Resource Server):你的应用是一个 API 服务,需要保护自己的资源。它要求来访的请求必须携带一个有效的访问令牌 (Access Token) 才能访问。
- OAuth 2.0 授权服务器 (Authorization Server):你的应用本身要成为一个 OAuth 2.0 提供方(像 Google 或 GitHub 那样),负责用户认证、颁发令牌。
重要提示:在过去,
spring-security-oauth
这个项目同时处理这三个角色。但它现在已被弃用。现代 Spring Security 使用全新的、独立的模块来处理这些场景,配置更简单、更安全。
- 客户端 ->
spring-boot-starter-oauth2-client
- 资源服务器 ->
spring-boot-starter-oauth2-resource-server
- 授权服务器 ->
spring-authorization-server
(一个独立的 Spring 社区项目)
下面我们分别详细说明前两种最常见场景的集成方法。
场景一:构建 OAuth 2.0 客户端 (Login with Google/GitHub)
目标:让用户通过外部身份提供商(IdP)登录我们的 Spring Boot 应用。
1. 添加依赖
在你的 pom.xml
中,确保有以下依赖:
<dependencies>
<!-- 核心安全依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- OAuth2 客户端支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
2. 配置 application.yml
这是配置的核心。你需要告诉 Spring Security 如何与外部提供商通信。以 Google 和 GitHub 为例:
spring:
security:
oauth2:
client:
# "registration" 下定义了所有支持的 OAuth2 提供商
registration:
google: # "google" 是一个自定义的注册ID
provider: google # Spring Boot 预设了常见提供商(google, github, facebook, okta),可省略大部分配置
client-id: YOUR_GOOGLE_CLIENT_ID
client-secret: YOUR_GOOGLE_CLIENT_SECRET
# scope 定义了你希望从 Google 获取哪些权限
scope:
- openid
- profile
- email
# 登录成功后,Google 会将用户重定向到这个地址,必须与你在Google Cloud Platform上配置的一致
# redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" # 这是默认值,可以不写
github: # "github" 是另一个注册ID
provider: github
client-id: YOUR_GITHUB_CLIENT_ID
client-secret: YOUR_GITHUB_CLIENT_SECRET
scope:
- read:user
# "provider" 部分可以用来定义非预设的、自定义的 OAuth2 服务器信息
# 如果使用Spring Boot预设的提供商,此部分可以省略
provider:
google:
# 这些URL都是Spring Boot预设好的,通常不需要手动配置
authorization-uri: https://accounts.google.com/o/oauth2/v2/auth
token-uri: https://www.googleapis.com/oauth2/v4/token
user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo
user-name-attribute: sub
# 自定义提供商的例子
# my-custom-provider:
# authorization-uri: ...
# token-uri: ...
# user-info-uri: ...
3. 配置安全规则 (SecurityFilterChain
)
在你的安全配置类中,启用 OAuth2 登录非常简单:
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 SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/", "/login**", "/error").permitAll() // 允许访问首页和登录相关页面
.anyRequest().authenticated() // 其他所有请求都需要认证
)
// 关键配置:启用 OAuth2 登录功能
.oauth2Login(oauth2 -> {
// 你可以在这里进行自定义配置,例如自定义登录页面、成功处理器等
// .loginPage("/login");
});
return http.build();
}
}
发生了什么?
oauth2Login()
这个方法会自动配置一个OAuth2LoginAuthenticationFilter
。- 当你访问受保护的页面时,它会自动重定向到 Spring Security 默认的登录选择页面 (
/login
),上面会显示你在application.yml
中配置的提供商(例如“Google”、“GitHub”)。 - 点击链接后,它会处理重定向到 Google/GitHub 的逻辑(授权码流程)。
- 它还会处理回调请求(
/login/oauth2/code/{registrationId}
),用授权码换取访问令牌,并获取用户信息。 - 最后,它会将用户信息包装成一个
OAuth2User
对象,并存入SecurityContextHolder
,完成登录。
4. 在 Controller 中获取用户信息
登录成功后,你可以很方便地在 Controller 中获取用户信息:
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@GetMapping("/user")
public String getUser(@AuthenticationPrincipal OAuth2User oauth2User) {
// OAuth2User 包含了从提供商获取的所有用户信息
String name = oauth2User.getAttribute("name");
String email = oauth2User.getAttribute("email");
return "Hello, " + name + "! Your email is " + email;
}
}
场景二:构建 OAuth 2.0 资源服务器 (Protected API)
目标:我们的应用是一个 REST API,需要验证客户端发来的 Bearer Token
(通常是 JWT),只有合法的令牌才能访问受保护的端点。
1. 添加依赖
<dependencies>
<!-- 核心安全依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- OAuth2 资源服务器支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
2. 配置 application.yml
你需要告诉资源服务器如何验证 JWT。最现代、最简单的方式是提供授权服务器的 issuer-uri
。
spring:
security:
oauth2:
resourceserver:
jwt:
# 关键配置:JWT 的签发者 URI。
# Spring Security 会自动访问这个 URI 的 ".well-known/openid-configuration" 端点,
# 找到 jwks_uri,并从中获取用于验证 JWT 签名的公钥。
# 例如,对于 Keycloak,它可能是 http://localhost:8080/realms/my-realm
issuer-uri: https://your-authorization-server.com/auth/realms/your-realm
# 或者,如果你的授权服务器不支持 OIDC Discovery,你可以直接指定公钥集地址:
# jwk-set-uri: https://your-authorization-server.com/auth/realms/your-realm/protocol/openid-connect/certs
3. 配置安全规则 (SecurityFilterChain
)
配置非常简洁,只需启用资源服务器支持即可。
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 SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/public").permitAll() // 公开访问的端点
// 所有 /api/** 的请求都需要认证,并且需要拥有 "read_profile" 的 scope
.requestMatchers("/api/user/**").hasAuthority("SCOPE_read_profile")
.anyRequest().authenticated()
)
// 关键配置:启用 OAuth2 资源服务器支持,并指定使用 JWT 进行验证
.oauth2ResourceServer(oauth2 -> oauth2.jwt());
return http.build();
}
}
发生了什么?
oauth2ResourceServer(oauth2 -> oauth2.jwt())
会配置一个BearerTokenAuthenticationFilter
。- 这个过滤器会拦截所有请求,检查
Authorization
头是否存在Bearer <token>
。 - 如果存在,它会提取 JWT,并使用从
issuer-uri
获取的公钥来验证 JWT 的签名、过期时间 (exp)、签发者 (iss) 等信息。 - 验证成功后,它会解析 JWT 中的
scope
或scp
声明,并将其转换为 Spring Security 的GrantedAuthority
(例如,"read_profile"
会被转换为SCOPE_read_profile
)。 - 然后,你就可以使用
.hasAuthority()
或@PreAuthorize
等标准 Spring Security 注解来进行方法级别的权限控制。
4. 在 Controller 中访问令牌信息
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/user")
public class UserApiController {
@GetMapping("/info")
public String getUserInfo(@AuthenticationPrincipal Jwt jwt) {
// Jwt 对象包含了令牌的所有声明 (claims)
String userId = jwt.getSubject(); // 'sub' claim
String issuer = jwt.getIssuer().toString();
// 访问自定义声明
String customClaim = jwt.getClaimAsString("custom_claim");
return "User ID: " + userId + " from issuer: " + issuer;
}
}
总结
角色 | 目的 | 关键依赖 | 核心配置 (.yml ) | 核心Java配置 |
---|---|---|---|---|
OAuth2 客户端 | 使用外部服务登录/授权 | spring-boot-starter-oauth2-client | spring.security.oauth2.client.registration (配置 client-id, secret, scope) | .oauth2Login() |
OAuth2 资源服务器 | 保护自己的API,验证令牌 | spring-boot-starter-oauth2-resource-server | spring.security.oauth2.resourceserver.jwt.issuer-uri (配置令牌签发者) | .oauth2ResourceServer(c -> c.jwt()) |