1. 前言(OAuth2 简介)

目前 OAuth2.0 已成为现代应用认证授权的标准,从单点登录到微服务架构都依赖其构建安全通道。我们常见的微信、QQ、微博等应用均有使用。OAuth 2.0(Open Authorization)是一种开放的授权框架,允许应用在不暴露用户密码的前提下,安全地代表用户访问第三方服务上的受保护资源。

本章节,笔者将带领大家深入解析四种核心授权模式,并基于 Spring Security 6 手把手搭建安全的资源服务器,助您掌握分布式系统的认证精髓。

注:部分介绍参考了官方文档:https://docs.spring.io/spring-security/reference/servlet/oauth2/index.html,小伙伴们可自行阅读并查阅官方示例代码。


2. OAuth2 的角色

根据 RFC 6749 标准,OAuth 2.0 定义了四种角色,每个角色在授权流程中承担不同职责:

  • 资源所有者(Resource Owner):通常是终端用户,拥有受保护资源。
  • 客户端(Client):向用户请求授权,代表用户访问资源的应用,通常是一个 Web 或移动应用。
  • 授权服务器(Authorization Server):认证资源所有者身份并颁发访问令牌。
  • 资源服务器(Resource Server):托管资源,根据令牌的有效性和权限范围决定是否允许访问。

为帮助小伙伴们更好理解,笔者用一个现实生活中的场景(小区门禁系统)来类比 OAuth2.0 的工作原理,直观理解其角色与四种授权模式。

小区门禁系统类比示意图

场景:小区门禁系统

假设你住在一个需要刷卡或扫码进入的小区,门禁系统要求验证身份后才能开门。以下是 OAuth2.0 的四个角色映射:

  • 资源所有者(Resource Owner):住户(你),你是小区住户,拥有进出小区的权限。
  • 客户端(Client):快递员或访客,需要临时进入小区的人或设备(如快递员的小程序)。
  • 授权服务器(Authorization Server):物业中心,负责验证住户身份,并发放临时通行权限(令牌)。
  • 资源服务器(Resource Server):小区门禁闸机,接收令牌(如二维码或门禁卡),验证后决定是否放行。

OAuth2 的核心逻辑

  • 你(住户)不想直接给快递员门禁卡或密码,而是通过物业中心生成一个临时二维码(令牌),限制其权限(例如:仅允许进入 1 次,或仅在 30 分钟内有效)。
  • 令牌由物业中心发放,你可以随时在物业系统中吊销它。

3. OAuth 2.0 四种授权模式

不同场景下,OAuth 2.0 定义了四种核心授权模式,各有适用范围和安全考量。

3.1 授权码模式(Authorization Code Grant)

授权码模式,指第三方应用先申请一个授权码,然后再用该码获取令牌。
这种方式最常用、最复杂,也是最安全的,适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则储存在后端,且所有与资源服务器的通信都在后端完成。这样的前后端分离,可避免令牌泄漏。

  • 场景:适用于安全的服务端 Web 应用,例如电商网站在结算时需要访问支付平台 API。
  • 流程
    1. 客户端引导用户浏览器跳转到授权服务器授权端点;
    2. 用户登录并同意后,授权服务器重定向携带 code 到客户端回调 URL;
    3. 客户端使用该 code 向令牌端点申请访问令牌,并在请求中携带客户端凭证。
  • 优点:令牌通过后端交换,避免在浏览器中暴露,安全性较高。

授权码模式流程示意图

现实生活模拟场景
还是以小区门禁系统为例:快递员通过「小区快递 App」申请进入权限。

  1. 快递员点击 App「申请进入小区」,App 跳转到物业的授权页面。
  2. 你(住户)在物业 App 上接收到授权通知,并同意授权(例如:允许快递员在 30 分钟内进入小区 1 次)。
  3. 物业中心生成一个授权码返回给快递员的快递 App。
  4. 快递 App 用授权码向物业中心换取一个临时二维码(令牌)。
  5. 快递员用二维码扫码进入小区门禁(资源服务器)。

3.2 隐式模式(Implicit Grant)

  • 场景:传统的单页应用(SPA)或无法安全存储客户端密钥的纯前端应用。
  • 流程:用户同意授权后,授权服务器直接在重定向 URI 的 fragment 中返回访问令牌,无需第二次交换。
  • 缺点:令牌暴露在浏览器,易受截取攻击。注:Spring Security 6 已移除隐式模式支持,建议使用 PKCE 增强的授权码模式。
1
2
3
4
// Spring Security 6 已移除隐式模式支持
http.oauth2Client(oauth2 -> oauth2
.authorizationCodeGrant(Customizer.withDefaults())
)

现实生活模拟场景
访客临时申请进入小区:

  1. 访客在门禁屏幕上点击「申请临时通行」。
  2. 直接跳转到物业授权页面,你登录并同意授权。
  3. 物业中心直接返回一个临时二维码(令牌)到门禁屏幕的 URL 中。
  4. 访客用二维码扫码进入,但二维码仅在 5 分钟内有效。

3.3 客户端凭证模式(Client Credentials Grant)

凭证式模式,也叫客户端模式,适用于没有前端的命令行应用,即在命令行下请求令牌。
这种方式给出的令牌是针对第三方应用的,而不是针对用户的,即可能多个用户共享同一个令牌。

  • 场景:应用间 M2M 通信,如后台微服务之间相互调用私有 API。
  • 流程:客户端直接向令牌端点发送 grant_type=client_credentials 请求,并在 HTTP Basic Auth 中提供客户端 ID/密钥,即可获取访问令牌。
  • 特点:不涉及用户,适合服务账户场景,配置简单且安全性可控。

客户端凭证模式流程示意图

现实生活模拟场景
物业巡逻车需要进出小区所有区域:

  1. 物业巡逻车用自己的工牌(客户端 ID 和密钥)向物业中心申请令牌。
  2. 物业中心验证工牌后,发放一个万能通行令牌(可进入所有区域)。
  3. 巡逻车凭令牌自由进出小区门禁。

3.4 资源所有者密码凭证模式(Resource Owner Password Credentials Grant)

密码式模式:如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码直接告诉该应用,该应用使用你的密码申请令牌。
这种方式需要用户给出自己的用户名/密码,风险较大,因此只适用于其他授权方式都无法采用的情况,且必须是用户高度信任的应用。

  • 场景:高度信任的客户端(如官方移动端应用)与授权服务器共属一个安全域时,可直接收集用户名和密码。
  • 流程:客户端将用户名、密码与自身凭证一起 POST 到令牌端点,授权服务器验证后颁发令牌。
  • 风险:客户端需处理用户敏感凭证,安全性最低,通常仅在无法采用授权码模式时作为后备方案。

密码模式流程示意图

现实生活模拟场景
你的家人定期出入小区(物业通过你的账号密码给其他人授权):

  1. 你直接将门禁账号密码授权给家人。
  2. 家人用你的账号密码向物业中心申请长期有效的通行令牌。
  3. 物业返回令牌,家人凭令牌进出小区。

3.5 总结

模式 门禁场景类比 权限范围 风险
授权码模式 快递员通过 App 申请临时通行 限时、限次 低(令牌不暴露)
隐式模式 访客直接扫码获得短期通行 超短时效 中(URL 暴露令牌)
密码模式 自家家人长期通行 长期有效 高(需信任设备)
客户端凭证模式 物业巡逻车自由通行 全权限 中(客户端可控)

4. 授权服务器与资源服务器搭建

讲解了 OAuth2 的四种授权模式,下面我们来实现授权服务器与资源服务器的搭建。

❶ 引入依赖

Spring Boot 3 + Spring Security 6 项目中,添加以下依赖即可快速构建授权服务器与资源服务器:

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

❷ 授权服务器配置

使用 OAuth2AuthorizationServerConfigurer 和基于内存的 RegisteredClientRepository 快速配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Configuration
public class AuthServerConfig {

@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("web-client")
.clientSecret("{noop}web-secret")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("http://localhost:8080/login/oauth2/code/web-client")
.scope("read")
.build();
return new InMemoryRegisteredClientRepository(client);
}

@Bean
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer<HttpSecurity> configurer = new OAuth2AuthorizationServerConfigurer<>();
http.apply(configurer)
.and()
.exceptionHandling(ex -> ex.authenticationEntryPoint(
new LoginUrlAuthenticationEntryPoint("/login")
));
return http.build();
}
}

该配置启用了授权码与客户端凭证模式,并自动暴露 /oauth2/authorize/oauth2/token 等端点。

❸ 资源服务器配置

application.yml 中指定授权服务器的 issuer-uri

1
2
3
4
5
6
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:9000

并通过 Lambda DSL 定义安全规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class ResourceServerConfig {

@Bean
public SecurityFilterChain resourceServerSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
}

此时,所有 /api/** 请求均需携带合法的 JWT 访问令牌,否则会返回 401/403 错误。

❹ 完整示例代码

结合上述讲解,完整代码展示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@SpringBootApplication
public class OAuth2ServerApplication {
public static void main(String[] args) {
SpringApplication.run(OAuth2ServerApplication.class, args);
}

// 注册客户端
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient appClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("app-client")
.clientSecret("{noop}app-secret")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("http://localhost:8080/login/oauth2/code/app-client")
.scope("read")
.build();
return new InMemoryRegisteredClientRepository(appClient);
}

// 授权服务器安全链
@Bean
@Order(1)
public SecurityFilterChain authServerFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer<HttpSecurity> oauth2Config = new OAuth2AuthorizationServerConfigurer<>();
http.apply(oauth2Config)
.and()
.csrf(csrf -> csrf.ignoringRequestMatchers("/oauth2/token"))
.exceptionHandling(ex -> ex.authenticationEntryPoint(
new LoginUrlAuthenticationEntryPoint("/login")
));
return http.build();
}

// 资源服务器安全链
@Bean
@Order(2)
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
}

在此示例中,同一应用既扮演授权服务器,也作为资源服务器提供 /api/** 保护接口,启动后即可体验完整的 OAuth 2.0 流程。


结语:构建开放安全的认证生态

通过本章节的讲解与实战示例,相信小伙伴们已掌握 OAuth 2.0 的核心概念、四大角色、四种授权模式及其在 Spring Security 6 中的配置方式。后续可根据业务需求,引入 PKCE、刷新令牌、动态客户端注册等高级特性,进一步提升系统安全性与可扩展性。