此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Authorization Server 1.4.2! |
作方法:通过 PKCE 使用单页应用程序进行身份验证
本指南介绍如何配置 Spring Authorization Server 以支持具有代码交换证明密钥 (PKCE) 的单页应用程序 (SPA)。 本指南的目的是演示如何支持公共客户端并要求 PKCE 进行客户端身份验证。
Spring Authorization Server 不会为公共客户端颁发刷新令牌。我们建议使用前端后端 (BFF) 模式作为公开公共客户端的替代方法。有关更多信息,请参阅 gh-297。 |
启用 CORS
SPA 由静态资源组成,这些资源可以通过多种方式进行部署。 它可以与后端分开部署,例如使用 CDN 或单独的 Web 服务器,也可以使用 Spring Boot 与后端一起部署。
当 SPA 托管在不同的域下时,可以使用跨域资源共享 (CORS) 来允许应用程序与后端通信。
例如,如果你有一个 Angular 开发服务器在端口4200
中,您可以定义CorsConfigurationSource
@Bean
并将 Spring Security 配置为允许使用cors()
DSL 的调用,如以下示例所示:
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;
}
}
Click on the "Expand folded text" icon in the code sample above to display the full example.
Configure a Public Client
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).
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
@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);
}
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.
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.
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 the code_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 the redirect_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 the code_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.