操作方法:通过 PKCE 使用单页应用程序进行身份验证

本指南介绍如何配置 Spring Authorization Server 以支持具有代码交换证明密钥 (PKCE) 的单页应用程序 (SPA)。 本指南的目的是演示如何支持公共客户端并要求 PKCE 进行客户端身份验证。spring-doc.cn

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

启用 CORS

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

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

例如,如果您在 port 上本地运行了 Angular 开发服务器,则可以定义并配置 Spring Security 以允许使用 DSL 的飞行前请求,如以下示例所示:4200CorsConfigurationSource@Beancors()spring-doc.cn

启用 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.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 {
		OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
				OAuth2AuthorizationServerConfigurer.authorizationServer();

		http
			.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
			.with(authorizationServerConfigurer, (authorizationServer) ->
				authorizationServer
					.oidc(Customizer.withDefaults())	// Enable OpenID Connect 1.0
			)
			// Redirect to the login page when not authenticated from the
			// authorization endpoint
			.exceptionHandling((exceptions) -> exceptions
				.defaultAuthenticationEntryPointFor(
					new LoginUrlAuthenticationEntryPoint("/login"),
					new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
				)
			);

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

}
单击上面代码示例中的“Expand folded text”图标以显示完整示例。

配置公共客户端

SPA 无法安全地存储凭据,因此必须将其视为公共客户端。 应要求公共客户端使用代码交换证明密钥 (PKCE)。spring-doc.cn

继续前面的示例,你可以将 Spring 授权服务器配置为使用客户端身份验证方法支持公共客户端,并需要 PKCE,如下例所示:nonespring-doc.cn

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);
}
该设置对于防止 PKCE 降级攻击非常重要。requireProofKey

使用客户端进行身份验证

将服务器配置为支持公共客户端后,一个常见问题是:如何对客户端进行身份验证并获取访问令牌?简短的回答是:与任何其他客户相同。spring-doc.cn

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

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

  1. 客户端通过重定向到授权端点来启动 OAuth2 请求。对于公共客户端,此步骤包括生成和计算 ,然后将其作为查询参数发送。code_verifiercode_challengespring-doc.cn

  2. 如果用户未通过身份验证,授权服务器将重定向到登录页面。身份验证后,用户将再次重定向回授权终端节点。spring-doc.cn

  3. 如果用户未同意请求的范围并且需要同意,则会显示同意页面。spring-doc.cn

  4. 用户同意后,授权服务器会生成一个,并通过 重定向回客户端。authorization_coderedirect_urispring-doc.cn

  5. 客户端通过 query 参数获取 ,并执行对 Token Endpoint 的请求。对于公有客户端,此步骤包括发送参数而不是凭证进行身份验证。authorization_codecode_verifierspring-doc.cn

正如你所看到的,流程相当复杂,这个概述只是触及了皮毛。spring-doc.cn

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