1. 概述

本文将详细介绍 如何使用 Spring Security 5 构建一个 OAuth 2.0 资源服务器

我们将分别实现两种主流的令牌验证方式:

✅ JWT(JSON Web Token)
✅ Opaque Token(不透明令牌)

在进入代码实现之前,先快速梳理一下背景知识,确保理解底层逻辑。毕竟踩过坑才知道,光会配置不理解原理,出问题时根本无从下手。


2. 背景知识

2.1 JWT 与 Opaque Token 的区别

  • JWT:是一种自包含的令牌格式(遵循 RFC 7519),以 JSON 结构明文携带用户信息、过期时间、签发者等元数据。资源服务器可直接解析并验证签名,无需每次都调用授权服务器。
  • Opaque Token:顾名思义,内容对外“不透明”,只是一个随机字符串。它本身不包含任何有效信息,资源服务器必须通过 Token Introspection 接口向授权服务器发起请求,查询该令牌是否有效、权限如何。

🔍 简单粗暴理解:JWT 是“自带身份证”,Opaque Token 是“需要联网查号”。

2.2 什么是资源服务器(Resource Server)

在 OAuth 2.0 体系中,资源服务器是负责保护受保护资源的服务应用。它不负责发令牌,只负责验令牌。

当客户端携带 Authorization: Bearer <token> 请求资源时,资源服务器需验证以下几点:

  • ✅ 令牌是否由可信的授权服务器签发
  • ✅ 是否已过期
  • ✅ 当前服务是否是目标受众(audience)
  • ✅ 是否具备访问该资源所需的权限(如 scope)

下面是典型的授权码流程时序图,我们重点关注第 8、9 步:

AuthCodeFlowSequenceDiagram

图中第 8 步:客户端调用资源服务器
第 9 步:资源服务器向授权服务器验证令牌(本文重点)


3. 授权服务器搭建(Keycloak 嵌入式)

我们使用 Keycloak 作为授权服务器。它是开源的身份与访问管理方案,支持 OpenID Connect 和 OAuth 2.0。

💡 本文重点在资源服务器,因此 Keycloak 的配置细节不再展开。感兴趣可参考:《Spring Boot 集成嵌入式 Keycloak》

我们预设了两个客户端:

  • fooClient → 对应 JWT 资源服务器
  • barClient → 对应 Opaque Token 资源服务器

Keycloak 运行在 http://localhost:8083/auth/realms/baeldung


4. 资源服务器 – 使用 JWT

4.1 Maven 依赖

核心依赖是 spring-boot-starter-oauth2-resource-server,它自动引入 Spring Security 并提供资源服务器支持。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.7.5</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    <version>2.7.5</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.13.0</version>
</dependency>

⚠️ 注意:无需显式引入 spring-security-*,starter 已包含。


4.2 模型类(Model)

定义一个简单的 POJO 作为受保护资源:

public class Foo {
    private long id;
    private String name;

    // 构造函数、getter、setter 省略
}

4.3 接口层(API)

提供 REST 接口供客户端操作 Foo 资源:

@RestController
@RequestMapping("/foos")
public class FooController {

    @GetMapping("/{id}")
    public Foo findOne(@PathVariable Long id) {
        return new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4));
    }

    @GetMapping
    public List<Foo> findAll() {
        List<Foo> fooList = new ArrayList<>();
        fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4)));
        fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4)));
        fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4)));
        return fooList;
    }

    @ResponseStatus(HttpStatus.CREATED)
    @PostMapping
    public void create(@RequestBody Foo newFoo) {
        logger.info("Foo created");
    }
}

支持:GET 单个、GET 列表、POST 创建。


4.4 安全配置(Security Configuration)

关键配置类,定义访问控制策略:

@Configuration
public class JWTSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests(authz -> authz
                .antMatchers(HttpMethod.GET, "/foos/**")
                    .hasAuthority("SCOPE_read")
                .antMatchers(HttpMethod.POST, "/foos")
                    .hasAuthority("SCOPE_write")
                .anyRequest()
                    .authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2.jwt());
        return http.build();
    }
}
  • GET 请求需具备 SCOPE_read 权限
  • POST 请求需具备 SCOPE_write 权限
  • .oauth2ResourceServer(oauth2 -> oauth2.jwt()) 明确声明支持 JWT 令牌

✅ 注意:Spring Security 5.4+ 使用 SecurityFilterChain 替代旧版 WebSecurityConfigurerAdapter


4.5 配置文件(application.yml)

server: 
  port: 8081
  servlet: 
    context-path: /resource-server-jwt

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8083/auth/realms/baeldung
  • issuer-uri 指定授权服务器地址,Spring 会自动获取其 OpenID Connect 提供商配置(如 JWK Set URI)
  • 启动时会自动拉取公钥用于验证 JWT 签名

⚠️ 若授权服务器未启动,资源服务器将无法初始化。若需独立部署,可改用 jwk-set-uri

jwk-set-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/certs

4.6 测试验证

使用 JUnit + RestAssured 模拟客户端请求:

@Test
public void givenUserWithReadScope_whenGetFooResource_thenSuccess() {
    String accessToken = obtainAccessToken("read"); // 从 Keycloak 获取带 read scope 的 token

    Response response = RestAssured.given()
      .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
      .get("http://localhost:8081/resource-server-jwt/foos");

    assertThat(response.as(List.class)).hasSizeGreaterThan(0);
}
  • ✅ 成功返回数据 → 表示 JWT 被正确验证并放行
  • 整个过程透明,开发者无需手动处理 JWT 解析

5. 资源服务器 – 使用 Opaque Token

5.1 Maven 依赖

Opaque Token 需要额外依赖用于 Token Introspection:

<dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>oauth2-oidc-sdk</artifactId>
    <version>8.19</version>
    <scope>runtime</scope>
</dependency>

⚠️ 注意:runtime 范围即可,编译期不需要。


5.2 模型与控制器

新增 Bar 资源:

public class Bar {
    private long id;
    private String name;
    // 构造函数、getter、setter
}

控制器 BarController 提供 /bars 接口,结构与 FooController 类似。


5.3 配置文件(application.yml)

server: 
  port: 8082
  servlet: 
    context-path: /resource-server-opaque

spring:
  security:
    oauth2:
      resourceserver:
        opaque:
          introspection-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token/introspect
          introspection-client-id: barClient
          introspection-client-secret: barClientSecret
  • introspection-uri:Keycloak 的令牌验证接口
  • 需提供客户端凭证用于调用该接口

5.4 安全配置

@Configuration
public class OpaqueSecurityConfig {

    @Value("${spring.security.oauth2.resourceserver.opaque.introspection-uri}")
    String introspectionUri;

    @Value("${spring.security.oauth2.resourceserver.opaque.introspection-client-id}")
    String clientId;

    @Value("${spring.security.oauth2.resourceserver.opaque.introspection-client-secret}")
    String clientSecret;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests(authz -> authz
                .antMatchers(HttpMethod.GET, "/bars/**")
                    .hasAuthority("SCOPE_read")
                .antMatchers(HttpMethod.POST, "/bars")
                    .hasAuthority("SCOPE_write")
                .anyRequest()
                    .authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2.opaqueToken(token ->
                token.introspectionUri(this.introspectionUri)
                     .introspectionClientCredentials(this.clientId, this.clientSecret)));
        return http.build();
    }
}
  • .opaqueToken() 表示启用不透明令牌支持
  • 显式配置 introspection 地址和客户端凭据

5.5 测试验证

测试 write 权限是否能成功创建资源:

@Test
public void givenUserWithWriteScope_whenPostNewBarResource_thenCreated() {
    String accessToken = obtainAccessToken("read write");
    Bar newBar = new Bar(Long.parseLong(randomNumeric(2)), randomAlphabetic(4));

    Response response = RestAssured.given()
      .contentType(ContentType.JSON)
      .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
      .body(newBar)
      .log().all()
      .post("http://localhost:8082/resource-server-opaque/bars");

    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED.value());
}
  • ✅ 返回 201 → 表示 Opaque Token 被成功验证并授权

6. 总结

本文完整演示了如何使用 Spring Security 5 构建两种类型的 OAuth 2.0 资源服务器:

方式 优点 缺点
✅ JWT 无状态、高性能、无需网络调用 无法主动吊销
✅ Opaque Token 可实时控制、支持吊销 每次请求需调用 introspection 接口,有性能开销

🎯 核心要点:

  • Spring Security 的 DSL 配置极其简洁
  • JWT 使用 .jwt(),Opaque 使用 .opaqueToken()
  • 生产环境建议结合缓存优化 Opaque Token 的验证性能

所有示例代码已托管至 GitHub:https://github.com/Baeldung/spring-security-oauth/tree/master/oauth-resource-server


原始标题:OAuth 2.0 Resource Server With Spring Security 5