Spring Security 中 OAuth 2.0 資源服務器

Spring Security
Remote
0
06:59 AM · Nov 30 ,2025

1. 概述

在本教程中,我們將學習如何使用 Spring Security 5 設置 OAuth 2.0 資源服務器

我們將使用 JWT 以及 Spring Security 支持的兩種類型的 bearer 令牌:匿名令牌和 JWT 令牌。

在開始實施和代碼示例之前,我們首先將建立一些背景知識。

2. A Little Background

2.1. What Are JWTs and Opaque Tokens?

JWT,或 JSON Web Token,是一種安全地在廣泛接受的 JSON 格式中傳輸敏感信息的手段。 包含的信息可能與用户有關,也可能與令牌本身有關,例如其過期時間和頒發者。

另一方面,一個不透明令牌,正如其名稱所暗示的,在它所攜帶的信息方面是透明的。 令牌只是一個標識符,指向存儲在授權服務器上的信息;它通過服務器端的事後驗證進行驗證。

2.2. What Is a Resource Server?

在 OAuth 2.0 的上下文中,資源服務器是指通過 OAuth 令牌保護資源的應用程序。 這些令牌通常由授權服務器頒發給客户端應用程序。 資源服務器的工作是驗證令牌在提供資源給客户端之前。

令牌的有效性取決於多個因素:

  • 此令牌是否來自配置的授權服務器?
  • 它是否未過期?
  • 此資源服務器是否是其預期受眾?
  • 令牌是否有足夠的權限訪問請求的資源?

為了可視化這一點,讓我們看一下 授權碼流程 的序列圖,並查看所有參與者:

AuthCodeFlowSequenceDiagram正如我們在步驟 8 中所看到的,當客户端應用程序調用資源服務器的 API 以訪問受保護的資源時,它首先會前往授權服務器以驗證請求中的令牌,然後響應客户端。

步驟 9 是本教程將重點關注的。

因此,現在讓我們進入代碼部分。 我們將使用 Keycloak 設置一個授權服務器,一個驗證 JWT 令牌的資源服務器,以及一些 JUnit 測試來模擬客户端應用程序並驗證響應。

3. 授權服務器

首先,我們將設置一個授權服務器,它負責頒發令牌。

為此,我們將使用嵌入在 Spring Boot 應用程序中的 Keycloak。Keycloak 是一個開源身份和訪問管理解決方案。由於本教程的重點是資源服務器,因此我們將不會深入研究它。

我們的嵌入式 Keycloak 服務器定義了兩個客户端,fooClientbarClient,它們對應於我們的兩個資源服務器應用程序。

4. 資源服務器 – 使用 JWT

我們的資源服務器將具有四個主要組件:

  • 模型 – 保護資源的資源
  • API – 暴露資源的 REST 控制器
  • 安全配置 – 定義受保護資源訪問控制的類,該類由 API 暴露
  • application.yml – 用於聲明屬性的文件,包括授權服務器的信息

在快速瞭解依賴項之後,我們將逐一處理這些組件,以處理資源服務器中的 JWT 令牌。

4.1. Maven 依賴項

主要,我們需要 spring-boot-starter-oauth2-resource-server,Spring Boot 為資源服務器支持提供的啓動器。此啓動器默認包含 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>

除此之外,我們還會添加 Web 支持。

為了我們的演示目的,我們將生成隨機資源,而不是從數據庫獲取它們,並藉助 Apache 的 commons-lang3 庫。

4.2. 模型

為了簡單起見,我們將使用 Foo,一個 POJO,作為我們的受保護資源:

public class Foo {
    private long id;
    private String name;
    
    // constructor, getters and setters
}

4.3. API

這是用於使 Foo 可供操作的 REST 控制器:

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

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

    @GetMapping
    public List findAll() {
        List 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");
    }
}

正如你所見,我們有 provision to GET all Foos,GET a Foo by id,and POST a Foo

4.4. 安全配置

在此配置類中,我們將定義我們資源的訪問級別:

@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();
    }
}

任何具有具有 read 範圍的訪問令牌的人都可以獲取 Foos。為了 POST 一個新的 Foo,其令牌應具有 write 範圍。

此外,我們將調用 jwt(),使用 oauth2ResourceServer() DSL 來指示我們服務器支持的令牌類型,此處

4.5. application.yml

在應用程序屬性中,除了常規端口號和上下文路徑之外,我們需要定義指向授權服務器的頒發者 URI,以便資源服務器可以發現其 提供者配置

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

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8083/auth/realms/baeldung

資源服務器使用此信息來驗證來自客户端應用程序的 JWT 令牌,如我們的序列圖中的步驟 9。

為了使驗證通過 issuer-uri 屬性工作,授權服務器必須正在運行。否則,資源服務器將無法啓動。

如果我們需要獨立啓動它,則我們可以提供 jwk-set-uri 屬性,以指向授權服務器的端點,該端點公開公共密鑰:

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

並且這就是我們使服務器驗證 JWT 令牌所需要的全部內容。

4.6. 測試

為了測試,我們將設置 JUnit。為了執行此測試,我們需要授權服務器,以及資源服務器正在運行。

讓我們驗證我們是否可以使用具有 read 範圍的訪問令牌從 resource-server-jwt 獲取 Foo

@Test
public void givenUserWithReadScope_whenGetFooResource_thenSuccess() {
    String accessToken = obtainAccessToken("read");

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

在上面的代碼中,在第 3 行,我們從授權服務器獲取具有 read 範圍的訪問令牌,涵蓋了從步驟 1 到 7 的序列圖。

步驟 8 由 RestAssuredget() 調用執行。步驟 9 由資源服務器與我們所見的方式執行,並且對我們來説是透明的。

5. 資源服務器 – 使用非透明令牌

接下來,讓我們看看對於我們處理非透明令牌的相同組件。

5.1. Maven 依賴項

為了支持非透明令牌,我們需要添加 oauth2-oidc-sdk 依賴項:


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

5.2. 模型和控制器

對於這個,我們將添加一個 Bar 資源:

public class Bar {
    private long id;
    private String name;
    
    // constructor, getters and setters
}

我們還將有一個 BarController, 具有與我們 FooController 相似的端點,用於向 Bar 資源提供 Bar 資源。

5.3. application.yml

application.yml 中,我們需要添加與我們授權服務器的非侵入端點相對應的 introspection-uri。正如之前提到的,這正是如何驗證非透明令牌的方式:

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

5.4. 安全配置

保持與 Foo 資源的訪問級別相似,此配置類還調用了 opaqueToken(),使用 oauth2ResourceServer() DSL 來指示使用非透明令牌類型

@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();
    }
}

在這裏,我們還將指定與我們用於的授權服務器客户端的客户端憑據。我們已經在我們的 application.yml 中定義了這些。

5.5. 測試

我們將為基於非透明令牌的資源服務器設置一個 JUnit,類似於我們為 JWT 的設置。

在這個例子中,我們將檢查一個具有寫入權限的訪問令牌是否可以向 resource-server-opaque POST 一個 Bar 資源:

@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());
}

如果收到 CREATED 狀態碼,則表示資源服務器已成功驗證了非透明令牌併為我們創建了 Bar

6. 結論

在本文中,我們學習瞭如何配置基於 Spring Security 的資源服務器應用程序,用於驗證 JWTs 以及 opaque tokens。

正如我們所見,通過最小的設置,Spring 使我們能夠無縫地使用發行者驗證 tokens 並將資源發送到請求方(在本例中,一個 JUnit 測試)

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.