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 令牌保護資源的應用程序。 這些令牌通常由授權服務器頒發給客户端應用程序。 資源服務器的工作是驗證令牌在提供資源給客户端之前。
令牌的有效性取決於多個因素:
- 此令牌是否來自配置的授權服務器?
- 它是否未過期?
- 此資源服務器是否是其預期受眾?
- 令牌是否有足夠的權限訪問請求的資源?
為了可視化這一點,讓我們看一下 授權碼流程 的序列圖,並查看所有參與者:

步驟 9 是本教程將重點關注的。
因此,現在讓我們進入代碼部分。 我們將使用 Keycloak 設置一個授權服務器,一個驗證 JWT 令牌的資源服務器,以及一些 JUnit 測試來模擬客户端應用程序並驗證響應。
3. 授權服務器
首先,我們將設置一個授權服務器,它負責頒發令牌。
為此,我們將使用嵌入在 Spring Boot 應用程序中的 Keycloak。Keycloak 是一個開源身份和訪問管理解決方案。由於本教程的重點是資源服務器,因此我們將不會深入研究它。
我們的嵌入式 Keycloak 服務器定義了兩個客户端,fooClient 和 barClient,它們對應於我們的兩個資源服務器應用程序。
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 由 RestAssured 的 get() 調用執行。步驟 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 測試)。