如何:使用帶 PKCE 的單頁應用程式進行身份驗證
本指南展示瞭如何配置 Spring Authorization Server 以支援帶有 PKCE (Proof Key for Code Exchange) 的單頁應用程式 (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;
}
}
| 單擊上面程式碼示例中的“展開摺疊文字”圖示以顯示完整示例。 |
配置公共客戶端
繼續前面的示例,您可以配置 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引數而不是憑據進行身份驗證。
如您所見,這個流程相當複雜,此概述僅觸及表面。
| 建議您使用單頁應用程式框架支援的強大客戶端庫來處理授權碼流程。 |