如何:使用带有PKCE的单页应用进行身份验证

本指南展示了如何配置Spring授权服务器以支持使用Proof Key for Code Exchange(PKCE)的单页应用(SPA)。本指南的目的是演示如何支持公共客户端并要求客户端进行PKCE进行身份验证。

Spring授权服务器不会为公共客户端发放刷新令牌。我们建议使用后端为前端(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());	// 启用OpenID Connect 1.0
		http
			// 未经身份验证时重定向到登录页面
			// 从授权端点
			.exceptionHandling((exceptions) -> exceptions
				.defaultAuthenticationEntryPointFor(
					new LoginUrlAuthenticationEntryPoint("/login"),
					new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
				)
			)
			// 接受用户信息和/或客户端注册的访问令牌
			.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()
			)
			// 表单登录处理从
			// 授权服务器过滤器链重定向到登录页面
			.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授权服务器以支持使用客户端身份验证方法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);
}
在您忘记包含code_challengecode_challenge_method查询参数的情况下,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参数而不是凭据进行身份验证。

正如您所看到的,这个流程相当复杂,这个概述只是皮毛。

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