How-to: Authenticate using a Single Page Application with PKCE
本指南展示了如何配置Spring Authorization Server,以通过证明密钥代码交换(PKCE)支持单页应用程序(SPA)。本指南的目的是演示如何支持公有客户端并要求PKCE进行客户端认证。
This guide shows how to configure Spring Authorization Server to support a Single Page Application (SPA) with Proof Key for Code Exchange (PKCE). The purpose of this guide is to demonstrate how to support a public client and require PKCE for client authentication.
Spring Authorization Server 不会为公用客户端颁发刷新令牌。我们建议将后端用作前端 (BFF) 模式,作为公开公用客户端的替代方案。请参阅 gh-297以了解更多信息。 |
Spring Authorization Server will not issue refresh tokens for a public client. We recommend the backend for frontend (BFF) pattern as an alternative to exposing a public client. See gh-297 for more information. |
Enable CORS
SPA 由静态资源组成,可以通过多种方式部署。它可以与后端分开部署,例如与 CDN 或单独的 Web 服务器一起部署,或者也可以使用 Spring Boot 与后端一起部署。
A SPA consists of static resources that can be deployed in a variety of ways. It can be deployed separately from the backend such as with a CDN or separate web server, or it can be deployed along side the backend using Spring Boot.
当 SPA 托管在不同的域下时,可以使用跨源资源共享 (CORS) 来让应用程序与后端通信。
When a SPA is hosted under a different domain, Cross Origin Resource Sharing (CORS) can be used to allow the application to communicate with the backend.
例如,如果您在端口 4200
上本地运行 Angular 开发服务器,可以定义一个 CorsConfigurationSource
@Bean
并使用 cors()
DSL 配置 Spring Security 以允许预检请求,如下面的示例所示:
For example, if you have an Angular dev server running locally on port 4200
, you can define a CorsConfigurationSource
@Bean
and configure Spring Security to allow pre-flight requests using the cors()
DSL as in the following example:
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;
}
}
单击以上代码示例中“展开折叠文本”图标以显示完整示例。 |
Click on the "Expand folded text" icon in the code sample above to display the full example. |
Configure a Public Client
SPA 无法安全地存储凭证,因此必须将其视为 public client。应要求公有客户端使用 Proof Key for Code Exchange (PKCE)。
A SPA cannot securely store credentials and therefore must be treated as a public client. Public clients should be required to use Proof Key for Code Exchange (PKCE).
继续 earlier 示例,您可以按照以下示例配置 Spring 授权服务器以使用客户端身份验证方法 none
支持公有客户端并要求 PKCE:
Continuing the earlier example, you can configure Spring Authorization Server to support a public client using the Client Authentication Method none
and require PKCE as in the following example:
-
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。
The requireProofKey
setting is important to prevent the PKCE Downgrade Attack.
Authenticate with the Client
服务器配置为支持公用客户端后,一个常见问题是:我如何验证客户端并获取访问令牌? 简单的答案是:与处理任何其他客户端时相同的方法。
Once the server is configured to support a public client, a common question is: How do I authenticate the client and get an access token? The short answer is: The same way you would with any other client.
SPA 是一个基于浏览器的应用程序,因此使用与任何其他客户端相同的基于重定向的流程。此问题通常与一种期望相关,即可以通过 REST API 执行身份验证,而 OAuth2 中不存在这种情况。 |
A SPA is a browser-based application and therefore uses the same redirection-based flow as any other client. This question is usually related to an expectation that authentication can be performed via a REST API, which is not the case with OAuth2. |
更详细的答案需要了解 OAuth2 和 OpenID Connect 中涉及的流程,在本例中为授权代码流程。授权代码流程的步骤如下:
A more detailed answer requires an understanding of the flow(s) involved in OAuth2 and OpenID Connect, in this case the Authorization Code flow. The steps of the Authorization Code flow are as follows:
-
The client initiates an OAuth2 request via a redirect to the Authorization Endpoint. For a public client, this step includes generating the
code_verifier
and calculating thecode_challenge
, which is then sent as a query parameter. -
If the user is not authenticated, the authorization server will redirect to the login page. After authentication, the user is redirected back to the Authorization Endpoint again.
-
If the user has not consented to the requested scope(s) and consent is required, the consent page is displayed.
-
Once the user has consented, the authorization server generates an
authorization_code
and redirects back to the client via theredirect_uri
. -
The client obtains the
authorization_code
via a query parameter and performs a request to the Token Endpoint. For a public client, this step includes sending thecode_verifier
parameter instead of credentials for authentication.
正如您所看到的,此流程相当复杂,本概述只是浅尝辄止。
As you can see, the flow is fairly involved and this overview only scratches the surface.
建议您使用由单页应用程序框架支持的健壮客户端库来处理授权码流程。 |
It is recommended that you use a robust client-side library supported by your single-page app framework to handle the Authorization Code flow. |