Spring Security 中支持秘密客户端的 PKCE 支持

Spring Security
Remote
0
10:14 AM · Nov 30 ,2025

1. 簡介

在本教程中,我們將演示如何在 Spring Boot 秘密客户端應用程序中使用 PKCE。

2. 背景

Proof Key for Code Exchange (PKCE) 是 OAuth 協議的擴展,最初針對公共客户端,通常是 SPA Web 應用程序或移動應用程序。它作為 Authorization Code Grant 流的一部分使用,有助於緩解惡意第三方發起的一些攻擊

這些攻擊的主要向量是提供商已經建立用户身份後,使用 HTTP 重定向發送授權碼的步驟。根據場景,該授權碼可能泄露和/或被攔截,從而允許攻擊者使用它來獲取有效的訪問令牌。

一旦獲得該訪問令牌,攻擊者可以使用它來訪問受保護的資源,並將其視為合法所有者使用。例如,如果該訪問令牌與銀行賬户關聯,他們就可以訪問交易明細、投資組合價值或其他敏感信息。

3. PKCE 修改 OAuth

PKCE 機制對標準授權碼流程進行了幾處調整:

  • 客户端在初始授權請求中發送兩個附加參數:code_challengecode_challenge_method
  • 在最後一步,當客户端用授權碼換取訪問令牌時,還有一個新的參數:code_verifier

一個啓用了 PKCE 的客户端將採取以下步驟來實現該機制:

首先,它生成一個作為 code_verifier 參數使用的隨機字符串。 按照 RFC 7636,該字符串的長度至少為 43 個八進制字節,但小於 128 個八進制字節。關鍵在於使用安全的隨機生成器,例如 JVM 的 SecureRandom 或等效的。

除了長度之外,允許範圍也有限制:僅支持字母數字 ASCII 字符,以及幾個符號。

接下來,客户端將生成的價值轉換為 code_challenge 參數,使用支持的方法。 目前,規範 提及 了兩種轉換方法:plainS256

  • plain 只是一個無操作轉換,因此轉換後的值與 code_verifier 相同
  • S256 對應於 SHA-256 哈希算法,其結果編碼為 BASE64

客户端然後使用常規參數(client_idscopestate 等)構建 OAuth 授權 URL,並添加生成的 code_challengecode_challenge_method

3.1. 代碼挑戰驗證

在 OAuth 授權碼流程的最後一步,客户端將發送原始 code_verifier 值以及按照此流程定義的常規值。 服務器然後驗證 code_verifier,根據挑戰的方法:

  • 對於 plain 方法,code_verifier 和挑戰必須相同
  • 對於 S256 方法,服務器計算提供的值的 SHA-256 值的哈希,並在將其與原始挑戰進行比較之前將其編碼為 BASE64。

因此,PKCE 如何有效地對抗授權碼攻擊? 如前所述,這些通常針對授權服務器發出的重定向,其中包含授權碼,以進行工作。 但是,使用 PKCE,此信息不再足以完成流程,至少對於 S256 方法。

代碼-換取-令牌只在客户端提供授權碼 驗證器時發生,而驗證器從未出現在重定向中。

當然,當使用 plain 方法時,驗證器和挑戰相同,因此在實際應用中沒有使用此方法的必要性。

3.2. PKCE 用於秘密客户端

在 OAuth 2.0 中,PKCE 是可選的,主要用於移動和 Web 應用程序。 然而,即將推出的 OAuth 2.1 版本強制執行 PKCE,不僅適用於公共客户端,還適用於秘密客户端。

為了記住,秘密客户端通常是託管應用程序,在雲端或本地服務器上運行。 這樣的客户端也使用授權碼流程,但由於最終的代碼交換步驟發生在後端和授權服務器之間,用户代理(Web 或移動)永遠不會“看到”訪問令牌。

除此之外,步驟與公共客户端的案例相同。

4. Spring Security 對 PKCE 的支持

自 Spring Security 5.7 版本起,PKCE 已完全支持 Servlet 和 Reactive 風格的 Web 應用程序。但是,由於並非所有身份提供商都支持此擴展,該功能默認未啓用。 Spring Boot 應用程序必須使用框架的 2.7 或更高版本,並依賴於標準依賴管理。 這確保項目會選擇正確的 Spring Security 版本,以及其傳遞依賴項。

PKCE 支持位於 spring-security-oauth2-client 模塊中。 對於 Spring Boot 應用程序,使用相應的 starter 模塊是引入此依賴的最簡單方法:


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

可以從 Maven Central 下載最新版本的 這些依賴項

在依賴項到位後,我們需要自定義 OAuth 2.0 登錄過程以支持 PKCE。 對於 Reactive 應用程序,這意味着添加一個 SecurityWebFilterChain bean,該 bean 應用此設置:

@Bean
public SecurityWebFilterChain pkceFilterChain(ServerHttpSecurity http,
  ServerOAuth2AuthorizationRequestResolver resolver) {
    http.authorizeExchange(r -> r.anyExchange().authenticated());
    http.oauth2Login(auth -> auth.authorizationRequestResolver(resolver));
    return http.build();
}

關鍵步驟是設置自定義 ServerOAuth2AuthorizationRequestResolver 在登錄規範中。 Spring Security 使用該接口的實現來為給定客户端註冊信息構建 OAuth 授權請求。

幸運的是,我們不必實現此接口。 相反,我們可以使用可用的 DefaultServerOAuth2AuthorizationRequestResolver 類,該類允許我們應用進一步的自定義:

@Bean
public ServerOAuth2AuthorizationRequestResolver pkceResolver(ReactiveClientRegistrationRepository repo) {
    var resolver = new DefaultServerOAuth2AuthorizationRequestResolver(repo);
    resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce());
    return resolver;
}

在這裏,我們實例化請求解析器,傳遞一個 ReactiveClientRegistrationRepository 實例。 然後,我們使用 OAuth2AuthorizationRequestCustomizers.withPkce(), 該類提供添加 PKCE 參數到授權請求 URL 的所需邏輯。

5. Testing

為了測試我們的啓用了 PKCE 的應用程序,我們需要一個支持此擴展的授權服務器。在本教程中,我們將使用 Spring 授權服務器用於此目的。該項目是 Spring 的最新添加項,允許我們快速構建 OAuth 2.1/OIDC 兼容的授權服務器。

5.1. 授權服務器設置

在我們的實時測試環境中,授權服務器以與客户端分開的過程運行。該項目是一個標準的 Spring Boot Web 應用程序,我們已將其添加到相關的 Maven 依賴項中:


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
</dependency>

最新版本的 starterSpring Authorization Server 可從 Maven Central 下載。

為了正常工作,授權服務器需要我們提供幾個配置 Bean,包括一個 RegisteredClientRepository 和一個 UserDetailsService。對於我們的測試目的,我們可以使用兩者都具有固定測試值的內存實現。對於本教程,前者更相關:

@Bean 
public RegisteredClientRepository registeredClientRepository() {      
    var pkceClient = RegisteredClient
      .withId(UUID.randomUUID().toString())
      .clientId("pkce-client")
      .clientSecret("{noop}obscura")
      .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
      .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
      .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
      .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
      .scope(OidcScopes.OPENID)          
      .scope(OidcScopes.EMAIL)          
      .scope(OidcScopes.PROFILE)
      .clientSettings(ClientSettings.builder()
        .requireAuthorizationConsent(false)
        .requireProofKey(true)
        .build())
      .redirectUri("http://127.0.0.1:8080/login/oauth2/code/pkce")
      .build();
    
    return new InMemoryRegisteredClientRepository(pkceClient);
}

關鍵在於使用 clientSettings() 方法來強制使用 PKCE 用於特定客户端。我們通過傳遞一個帶有 ClientSettings 對象的 requireProofKey() 設置為 true 的對象來實現這一點。

在我們的測試設置中,客户端將在與授權服務器相同的主機上運行,因此我們使用 127.0.0.1 作為重定向 URL 中主機名的一部分。請注意,在這裏“localhost”不允許使用,因此我們使用等效的 IP 地址。

為了完成設置,我們還需要修改應用程序的屬性文件中默認端口設置:

server.port=8085

5.2. 運行實時測試

現在,讓我們運行一個實時測試,以驗證一切都按預期工作。我們可以直接從 IDE 中運行兩個項目,也可以打開兩個 shell 窗口並對每個模塊發出 mvn spring-boot:run 命令。無論使用哪種方法,一旦兩個應用程序都啓動,我們就可以打開瀏覽器並將其指向 http://127.0.0.1:8080

我們應該看到 Spring Security 的默認登錄頁面:

pkce sign in

請注意地址欄中的 URL:http://localhost:8085這意味着登錄表單來自授權服務器通過重定向。要驗證此聲明,可以在登錄表單上打開 Chrome 的 DevTools(或您選擇的瀏覽器的等效工具)並重新輸入地址欄中的初始 URL:

pkce challenge

我們可以看到 PKCE 參數在響應中存在,該響應由我們的客户端應用程序生成,併發送到 http://127.0.0.1:8080/oauth2/authorization/pkce 請求:

Location: http://localhost:8085/oauth2/authorize?
  response_type=code&
  client_id=pkce-client&
  scope=openid email&
  state=sUmww5GH14yatTwnv2V5Xs0rCCJ0vz0Sjyp4tK1tsdI=&
  redirect_uri=http://127.0.0.1:8080/login/oauth2/code/pkce&
  nonce=FVO5cA3_UNVVIjYnZ9ZrNq5xCTfDnlPERAvPCm0w0ek&
  code_challenge=g0bA5_PNDxy-bdf2t9H0ximVovLqMdbuTVxmGnXjdnQ&
  code_challenge_method=S256

為了完成登錄序列,我們將使用“user”和“password”作為憑據。如果我們繼續遵循請求,我們將看到代碼驗證器或訪問令牌從未存在,這是我們的目標。

6. 結論

在本教程中,我們演示瞭如何在 Spring Security 應用程序中使用 OAuth 的 PKCE 擴展,只需幾行代碼。 此外,我們還展示瞭如何使用 Spring Authorization Server 庫為測試目的創建一個定製的服務器。

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

發佈 評論

Some HTML is okay.