1. 簡介
在本教程中,我們將演示如何在 Spring Boot 秘密客户端應用程序中使用 PKCE。
2. 背景
Proof Key for Code Exchange (PKCE) 是一種擴展,最初針對公共客户端,通常是 SPA 網頁應用程序或移動應用程序。它作為 Authorization Code Grant 流的一部分使用,有助於緩解惡意第三方發起的一些攻擊。
當提供者已經確認了用户的身份並使用 HTTP 重定向發送授權碼時,這通常是攻擊的主要向量。根據具體情況,此授權碼可能會泄露和/或被攔截,從而允許攻擊者使用它獲取有效的訪問令牌。
一旦攻擊者獲得此訪問令牌,他們可以使用它來訪問受保護的資源,就像合法所有者一樣。例如,如果此訪問令牌與銀行賬户關聯,他們就可以訪問交易明細、投資組合價值或其他敏感信息。
3. PKCE 修改 OAuth 流程
PKCE 機制對標準授權碼流程添加了一些調整:
- 客户端在初始授權請求中發送兩個額外的參數:code_challenge 和 code_challenge_method
- 在最後一步,當客户端用授權碼換取訪問令牌時,也存在一個新的參數:code_verifier
一個啓用了 PKCE 的客户端將執行以下步驟來實現該機制:
首先,它生成一個用於作為 code_verifier 參數的隨機字符串。 根據 RFC 7636,該字符串的長度至少為 43 字節,小於 128 字節。關鍵在於使用安全的隨機生成器,例如 JVM 的 SecureRandom 或等效的實現。
除了長度之外,允許的字符範圍也有限制:僅支持字母數字 ASCII 字符,以及一些符號。
接下來,客户端將生成的價值轉換為 code_challenge 參數,使用支持的方法。 目前,規範 提及 了兩種轉換方法:plain 和 S256。
- plain 只是一個無操作轉換,轉換後的值與 code_verifier 相同
- S256 對應於 SHA-256 哈希算法,其結果用 BASE64 編碼
客户端然後使用標準的參數(client_id、scope、state 等)構建 OAuth 授權 URL,並添加生成的 code_challenge 和 code_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。對於反應式應用,這意味着添加一個 SecurityWebFilterChain 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. 測試
為了測試我們啓用了 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>
最新版本的 starter 和 Spring Authorization Server 可從 Maven Central 下載。
為了正常工作,Authorization Server 要求我們提供幾個配置 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=80855.2. 運行實時測試
現在,讓我們運行一個實時測試,以驗證所有內容是否按預期工作。我們可以直接從 IDE 中運行兩個項目,或者打開兩個 shell 窗口併為每個模塊發出命令 <em >mvn spring-boot:run</em >。 無論使用哪種方法,一旦兩個應用程序都啓動,我們就可以打開瀏覽器並將其指向http://127.0.0.1:8080。
我們應該看到 Spring Security 的默認登錄頁面:
請注意地址欄中的 URL: `http://localhost:8085。 這意味着登錄表單是通過重定向從授權服務器提供的。 要驗證此聲明,當在登錄表單上打開 Chrome 的 DevTools(或您選擇的瀏覽器的等效工具)並重新輸入地址欄中的初始 URL:
我們可以看到 PKCE 參數在客户端應用程序生成響應的 Location 標頭中存在,該響應針對 `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為了完成登錄序列,我們將使用“用户”和“密碼”作為憑據。如果我們繼續遵循請求,我們會發現代碼驗證器和訪問令牌從未出現,這正是我們的目標。
6. 結論
在本教程中,我們演示瞭如何在 Spring Security 應用程序中啓用 OAuth 的 PKCE 擴展,只需幾行代碼即可。此外,我們還展示瞭如何使用 Spring Authorization Server 庫為測試目的創建一個定製的服務器。