How-to: Authenticate using a Single Page Application with PKCE

本指南展示了如何配置Spring Authorization Server,以通过证明密钥代码交换(PKCE)支持单页应用程序(SPA)。本指南的目的是演示如何支持公有客户端并要求PKCE进行客户端认证。

Spring Authorization Server 不会为公用客户端颁发刷新令牌。我们建议将后端用作前端 (BFF) 模式,作为公开公用客户端的替代方案。请参阅 gh-297以了解更多信息。

Enable CORS

SPA 由静态资源组成,可以通过多种方式部署。它可以与后端分开部署,例如与 CDN 或单独的 Web 服务器一起部署,或者也可以使用 Spring Boot 与后端一起部署。

当 SPA 托管在不同的域下时,可以使用跨源资源共享 (CORS) 来让应用程序与后端通信。

例如,如果您在端口 4200 上本地运行 Angular 开发服务器,可以定义一个 CorsConfigurationSource @Bean 并使用 cors() DSL 配置 Spring Security 以允许预检请求,如下面的示例所示:

Enable 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;
	}

}

单击以上代码示例中“展开折叠文本”图标以显示完整示例。

Configure a Public Client

SPA 无法安全地存储凭证,因此必须将其视为 public client。应要求公有客户端使用 Proof Key for Code Exchange (PKCE)。

继续 earlier 示例,您可以按照以下示例配置 Spring 授权服务器以使用客户端身份验证方法 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
import java.util.UUID;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;

@Configuration
public class ClientConfig {

	// tag::client[]
	@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);
	}
	// end::client[]

}

`requireProofKey`设置非常重要,可以防止 PKCE Downgrade Attack

Authenticate with the Client

服务器配置为支持公用客户端后,一个常见问题是:我如何验证客户端并获取访问令牌? 简单的答案是:与处理任何其他客户端时相同的方法。

SPA 是一个基于浏览器的应用程序,因此使用与任何其他客户端相同的基于重定向的流程。此问题通常与一种期望相关,即可以通过 REST API 执行身份验证,而 OAuth2 中不存在这种情况。

更详细的答案需要了解 OAuth2 和 OpenID Connect 中涉及的流程,在本例中为授权代码流程。授权代码流程的步骤如下:

  1. 客户端通过重定向到 Authorization Endpoint 来发起 OAuth2 请求。对于公共客户端,此步骤包括生成 code_verifier 并计算 code_challenge,然后将其作为查询参数发送。

  2. 如果用户未经身份验证,授权服务器将重定向到登录页面。在身份验证后,用户会被重新定向回授权端点。

  3. 如果用户未同意所请求的范围(范围)且需要同意,则会显示同意页面。

  4. 一旦用户同意,授权服务器会生成一个 authorization_code 并通过 redirect_uri 将用户重定向回客户端。

  5. 客户端通过查询参数获得 authorization_code ,并对 Token Endpoint 执行请求。对于公共客户端,此步骤包括发送 code_verifier 参数,而不是认证证书。

正如您所看到的,此流程相当复杂,本概述只是浅尝辄止。

建议您使用由单页应用程序框架支持的健壮客户端库来处理授权码流程。