1. 簡介
在本教程中,我們將演示如何在 Spring Boot 秘密客户端應用程序中使用 PKCE。
2. 背景
Proof Key for Code Exchange (PKCE) 是 OAuth 協議的擴展,最初針對公共客户端,通常是 SPA Web 應用程序或移動應用程序。它作為 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。 對於 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>
最新版本的 starter 和 Spring 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 的默認登錄頁面:
請注意地址欄中的 URL:http://localhost:8085。這意味着登錄表單來自授權服務器通過重定向。要驗證此聲明,可以在登錄表單上打開 Chrome 的 DevTools(或您選擇的瀏覽器的等效工具)並重新輸入地址欄中的初始 URL:
我們可以看到 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 庫為測試目的創建一個定製的服務器。