操作指南:使用单页应用程序配合 PKCE 进行身份验证
本指南展示了如何配置 Spring Authorization Server 以支持使用授权码交换证明密钥 (PKCE) 的单页应用程序 (SPA)。本指南旨在演示如何支持公共客户端并要求使用 PKCE 进行客户端身份验证。
Spring Authorization Server 不会为公共客户端颁发刷新令牌。我们建议采用前端后端(BFF)模式作为公开公共客户端的替代方案。更多信息请参阅 gh-297。
启用 CORS
单页应用(SPA)由静态资源构成,可通过多种方式部署。它既可以与后端分离部署(例如使用CDN或独立Web服务器),也可以借助Spring Boot与后端共同部署。
当单页应用(SPA)部署在不同域名下时,可通过跨源资源共享(CORS)机制实现与后端服务的通信。
例如,若您在本地端口 4200 上运行 Angular 开发服务器,可以定义一个 CorsConfigurationSource @Bean,并通过 cors() DSL 配置 Spring Security 以允许预检请求,示例如下:
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
)
.authorizeHttpRequests((authorize) ->
authorize
.anyRequest().authenticated()
)
// 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;
}
}
点击上方代码示例中的 "展开折叠文本" 图标以显示完整示例。
配置公共客户端
继续之前的例子,你可以配置 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 所涉及的流程,在本例中即授权码流程。授权码流程的步骤如下:
-
客户端通过重定向到授权端点发起 OAuth2 请求。对于公共客户端,此步骤包括生成
code_verifier并计算code_challenge,然后将其作为查询参数发送。 -
如果用户未通过身份验证,授权服务器将重定向到登录页面。身份验证后,用户将再次被重定向回授权端点。
-
如果用户未同意请求的范围且需要同意,则会显示同意页面。
-
一旦用户同意,授权服务器将生成
authorization_code,并通过redirect_uri重定向回客户端。 -
客户端通过查询参数获取
authorization_code,并向令牌端点发起请求。对于公共客户端,此步骤包括发送code_verifier参数而不是凭据进行身份验证。
如你所见,整个流程相当复杂,这里的概述只是触及了表面。
建议使用由您的单页面应用框架支持的健壮客户端库来处理授权码流程。