操作指南:使用 PKCE 的單頁應用程式進行身份驗證

本指南說明如何配置 Spring Authorization Server 以支援使用 Proof Key for Code Exchange (PKCE) 的單頁應用程式 (SPA)。本指南的目的是示範如何支援公用用戶端,並要求 PKCE 用於用戶端身份驗證。

Spring Authorization Server 不會為公用用戶端發出重新整理令牌。我們建議使用後端服務於前端 (BFF) 模式,作為公開公用用戶端的替代方案。請參閱 gh-297 以取得更多資訊。

啟用 CORS

SPA 由靜態資源組成,這些資源可以透過多種方式部署。它可以與後端分開部署,例如使用 CDN 或獨立的 Web 伺服器,或者它可以與後端一起使用 Spring Boot 部署。

當 SPA 託管在不同的網域下時,可以使用跨來源資源共享 (CORS) 來允許應用程式與後端進行通訊。

例如,如果您在本機埠 4200 上執行 Angular 開發伺服器,您可以定義 CorsConfigurationSource @Bean,並配置 Spring Security 以允許使用 cors() DSL 進行預檢請求,如下列範例所示

啟用 CORS
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	@Order(1)
	public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
			throws Exception {
		OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
		http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
			.oidc(Customizer.withDefaults());	// Enable OpenID Connect 1.0
		http
			// Redirect to the login page when not authenticated from the
			// authorization endpoint
			.exceptionHandling((exceptions) -> exceptions
				.defaultAuthenticationEntryPointFor(
					new LoginUrlAuthenticationEntryPoint("/login"),
					new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
				)
			)
			// Accept access tokens for User Info and/or Client Registration
			.oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));

		return http.cors(Customizer.withDefaults()).build();
	}

	@Bean
	@Order(2)
	public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
			throws Exception {
		http
			.authorizeHttpRequests((authorize) -> authorize
				.anyRequest().authenticated()
			)
			// Form login handles the redirect to the login page from the
			// authorization server filter chain
			.formLogin(Customizer.withDefaults());

		return http.cors(Customizer.withDefaults()).build();
	}

	@Bean
	public CorsConfigurationSource corsConfigurationSource() {
		UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		CorsConfiguration config = new CorsConfiguration();
		config.addAllowedHeader("*");
		config.addAllowedMethod("*");
		config.addAllowedOrigin("http://127.0.0.1:4200");
		config.setAllowCredentials(true);
		source.registerCorsConfiguration("/**", config);
		return source;
	}

}
按一下上方程式碼範例中的「展開摺疊文字」圖示以顯示完整範例。

配置公用用戶端

SPA 無法安全地儲存憑證,因此必須視為公用用戶端。公用用戶端應被要求使用 Proof Key for Code Exchange (PKCE)。

繼續先前的範例,您可以配置 Spring Authorization Server 以支援使用用戶端身份驗證方法 none 的公用用戶端,並要求 PKCE,如下列範例所示

  • Yaml

  • Java

spring:
  security:
    oauth2:
      authorizationserver:
        client:
          public-client:
            registration:
              client-id: "public-client"
              client-authentication-methods:
                - "none"
              authorization-grant-types:
                - "authorization_code"
              redirect-uris:
                - "http://127.0.0.1:4200"
              scopes:
                - "openid"
                - "profile"
            require-authorization-consent: true
            require-proof-key: true
@Bean
public RegisteredClientRepository registeredClientRepository() {
	RegisteredClient publicClient = RegisteredClient.withId(UUID.randomUUID().toString())
		.clientId("public-client")
		.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
		.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
		.redirectUri("http://127.0.0.1:4200")
		.scope(OidcScopes.OPENID)
		.scope(OidcScopes.PROFILE)
		.clientSettings(ClientSettings.builder()
			.requireAuthorizationConsent(true)
			.requireProofKey(true)
			.build()
		)
		.build();

	return new InMemoryRegisteredClientRepository(publicClient);
}
requireProofKey 設定對於防止 PKCE 降級攻擊非常重要。

使用用戶端進行身份驗證

一旦伺服器配置為支援公用用戶端,常見的問題是:我該如何驗證用戶端並取得存取令牌? 簡短的答案是:與任何其他用戶端的方式相同。

SPA 是基於瀏覽器的應用程式,因此使用與任何其他用戶端相同的基於重新導向的流程。此問題通常與期望可以透過 REST API 執行身份驗證有關,但 OAuth2 並非如此。

更詳細的答案需要了解 OAuth2 和 OpenID Connect 中涉及的流程,在本例中為授權碼流程。授權碼流程的步驟如下

  1. 用戶端透過重新導向至 授權端點 來啟動 OAuth2 請求。對於公用用戶端,此步驟包括產生 code_verifier 並計算 code_challenge,然後將其作為查詢參數傳送。

  2. 如果使用者未通過身份驗證,授權伺服器將重新導向至登入頁面。身份驗證後,使用者將再次重新導向回授權端點。

  3. 如果使用者尚未同意請求的範圍,且需要同意,則會顯示同意頁面。

  4. 一旦使用者同意,授權伺服器會產生 authorization_code,並透過 redirect_uri 重新導向回用戶端。

  5. 用戶端透過查詢參數取得 authorization_code,並對 令牌端點 執行請求。對於公用用戶端,此步驟包括傳送 code_verifier 參數,而不是用於身份驗證的憑證。

如您所見,此流程相當複雜,而此總覽僅觸及表面。

建議您使用單頁應用程式框架支援的穩健用戶端程式庫來處理授權碼流程。