知識庫 / Spring / Spring Security RSS 訂閱

Spring Security 與 OpenID Connect

Spring Security
HongKong
5
02:37 PM · Dec 06 ,2025

請注意,本文已更新至新的 Spring Security OAuth 2.0 堆棧。儘管仍有使用舊堆棧的教程可用,但請知悉。

1. 概述

本教程將重點介紹如何使用 Spring Security 設置 OpenID Connect (OIDC)。

我們將探討 OIDC 規範的各個方面,並展示 Spring Security 在實現 OAuth 2.0 客户端支持的方案。

2. 快速 OpenID Connect 簡介

OpenID Connect 是建立在 OAuth 2.0 協議之上的身份層。

因此,在深入瞭解 OIDC 之前,瞭解 OAuth 2.0 非常重要,特別是授權碼流程。

OIDC 規範套件非常廣泛。它包含核心功能以及其他幾個可選能力,這些能力以不同的組呈現。

以下是主要內容:

  • 核心 – 使用 Claims 進行身份驗證和傳遞端用户信息
  • 發現 – 規定客户端如何動態確定關於 OpenID 提供的信息
  • 動態註冊 – 規定客户端如何與提供者註冊
  • 會話管理 – 定義如何管理 OIDC 會話

在此基礎上,文檔區分了提供對該規範支持的 OAuth 2.0 身份驗證服務器,並將它們稱為 OpenID 提供者 (OPs),以及使用 OIDC 作為依賴方的 OAuth 2.0 客户端,並將它們稱為依賴方 (RPs)。 在本文中,我們將使用這些術語。

還值得注意的是,客户端可以通過在授權請求中添加 openid 範圍來請求使用此擴展。

最後,對於本教程,知道 OpenID 提供者會發出端用户信息作為 JWT,即 ID 令牌,會很有幫助。

現在,我們準備好深入探索 OIDC 世界。

3. 項目設置在開始實際開發之前,我們需要在我們的身份提供商處註冊一個 OAuth 2.0 客户端。

在本例中,我們將 Google 用作身份提供商。 按照 這些説明 在其平台上註冊我們的客户端應用程序。 請注意,openid 範圍默認存在。

在這個過程中設置的重定向 URI 是我們服務中的一個端點:http://localhost:8081/login/oauth2/code/google

我們應該從這個過程中獲取 Client ID 和 Client Secret。

3.1. Maven 配置

我們將首先將這些依賴項添加到我們的項目 pom 文件中:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
    <version>3.1.5</version>
</dependency>

啓動包聚合了所有 Spring Security Client 相關依賴項,包括

  • spring-security-oauth2-client 依賴項,用於 OAuth 2.0 登錄和客户端功能
  • JOSE 庫,用於 JWT 支持

如往常一樣,可以使用 Maven 中央倉庫搜索引擎 查找最新版本。

4. 使用 Spring Boot 進行基本配置

首先,我們將配置應用程序使用我們剛剛創建的 Google 客户端註冊功能。

使用 Spring Boot 可以極大地簡化此過程,我們只需要定義兩個應用程序屬性即可

spring:
  security:
    oauth2:
      client:
        registration: 
          google: 
            client-id: <client-id>
            client-secret: <secret>

讓我們啓動我們的應用程序,並嘗試訪問一個端點。我們會看到我們被重定向到我們的 OAuth 2.0 客户端的 Google 登錄頁面。

它看起來很簡單,但實際上這裏有很多事情在發生。接下來,我們將探索 Spring Security 如何實現這一點。

在之前的“WebClient 和 OAuth 2 支持”文章中,我們分析了 Spring Security 如何處理 OAuth 2.0 授權服務器和客户端的內部機制。

在那篇文章中,我們發現除了客户端 ID 和客户端密鑰之外,還需要提供額外的數據才能成功配置 ClientRegistration 實例。

那麼,它是如何工作的呢?

Google 是一家知名提供商,因此該框架提供了一些預定義的屬性以簡化操作。

我們可以查看這些配置在 CommonOAuth2Provider 枚舉中。

對於 Google,該枚舉類型定義了諸如以下屬性:

  1. 默認的權限範圍
  2. 授權端點
  3. 令牌端點
  4. 用户信息端點,這部分也是 OIDC 核心規範的一部分

4.1. 訪問用户信息

Spring Security 提供了一個有用的用户 Principal 表示形式,該表示形式與 OIDC 提供程序註冊的,即 OidcUser 實體。

除了基本的 OAuth2AuthenticatedPrincipal 方法之外,此實體還提供了一些有用的功能:

  • 檢索 ID 令牌的值以及它包含的聲明
  • 獲取來自 UserInfo 端點的聲明
  • 生成這兩種集合的聚合

我們可以輕鬆地在控制器中訪問此實體:

@GetMapping("/oidc-principal")
public OidcUser getOidcUserPrincipal(
  @AuthenticationPrincipal OidcUser principal) {
    return principal;
}

當然,以下是翻譯後的內容:

或者,我們也可以在 Bean 中使用 SecurityContextHolder

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication.getPrincipal() instanceof OidcUser) {
    OidcUser principal = ((OidcUser) authentication.getPrincipal());
    
    // ...
}

如果我們檢查負責人,就能看到很多有用的信息,例如用户的姓名、電子郵件、個人資料圖片和地區設置。

5. OIDC 在實踐中

迄今為止,我們已經學習瞭如何使用 Spring Security 輕鬆實現 OIDC 登錄解決方案。

我們已經看到了它通過將用户身份驗證過程委派給 OpenID 提供者所帶來的好處,該提供者可以提供詳細且有用的信息,即使在可擴展的環境中也是如此。

但事實是,我們此前沒有處理過任何 OIDC 相關的方面。 這意味着 Spring 在為我們完成大部分工作。

因此,讓我們看看幕後發生了什麼,以便更好地理解規範如何應用於實踐,並充分利用它。

5.1. 登錄流程

為了更清楚地瞭解,我們啓用 RestTemplate 的日誌記錄,以便查看服務執行的請求:

logging:
  level:
    org.springframework.web.client.RestTemplate: DEBUG

如果現在調用一個安全端點,我們會看到服務正在執行標準的 OAuth 2.0 授權碼流程。這是因為,正如我們所説,該規範建立在 OAuth 2.0 的基礎上。

存在一些差異。

首先,根據我們使用的提供商和配置的權限範圍,我們可能會看到服務向 UserInfo 端點發出調用,正如我們之前提到的。

具體來説,如果授權響應檢索到至少一個 profileemailaddressphone 權限範圍,則框架將調用 UserInfo 端點以獲取更多信息。

儘管表明 Google 應該檢索 profileemail 權限範圍——因為我們在授權請求中使用它們——但 OP 檢索了他們的自定義對應項,即 https://www.googleapis.com/auth/userinfo.emailhttps://www.googleapis.com/auth/userinfo.profile,因此 Spring 不會調用該端點。

這意味着我們獲取的所有信息都包含在 ID 令牌中。

我們可以通過創建和提供自己的 OidcUserService 實例來適應這種行為:

@Configuration
public class OAuth2LoginSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        Set<String> googleScopes = new HashSet<>();
        googleScopes.add("https://www.googleapis.com/auth/userinfo.email");
        googleScopes.add("https://www.googleapis.com/auth/userinfo.profile");

        OidcUserService googleUserService = new OidcUserService();
        googleUserService.setAccessibleScopes(googleScopes);

        http.authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
            .oauth2Login(oauthLogin -> oauthLogin.userInfoEndpoint(userInfoEndpointConfig ->
                    userInfoEndpointConfig.oidcUserService(googleUserService)));
        return http.build();
    }
}

我們接下來觀察到的第二個差異是調用 JWK Set URI。正如我們在 JWT 格式 ID Token 簽名、JWK 帖子中所解釋的,它用於驗證 JWT 格式的 ID Token 簽名。

接下來,我們將對 ID Token 進行詳細分析。

5.2 身份令牌 (ID Token)

當然,OIDC 規範涵蓋並適應了各種不同的場景。在本例中,我們使用授權碼流程,協議表明訪問令牌和身份令牌將在令牌端點響應中檢索出來。

正如我們之前所説,OidcUser 實體包含身份令牌中包含的聲明,以及格式為 JWT 的實際令牌,可以使用 jwt.io 進行檢查。

此外,Spring 提供了許多方便的 getter 方法,以簡潔的方式獲取規範定義的標準聲明。

我們可以看到身份令牌包含一些強制聲明:

  • 頒發者標識符,格式為 URL(例如:https://accounts.google.com
  • 主題 ID,即頒發者包含的終端用户的引用
  • 令牌的過期時間
  • 令牌簽發時間
  • 受眾,其中包含我們配置的 OAuth 2.0 客户端 ID

它還包含許多 標準聲明,例如我們之前提到的 (name, locale, picture, email)。

由於這些是標準聲明,因此我們可以預期許多提供者將檢索至少一些這些字段,從而簡化解決方案的開發。

5.3. 聲明與範圍

正如我們可以想象的,OP檢索到的聲明與我們(或Spring Security)配置的範圍相對應。

OIDC定義了一組可以用於請求OIDC聲明的範圍:

  • profile,可用於請求默認的 profile 聲明(例如:namepreferred_usernamepicture 等)
  • email,用於訪問 emailemail_verified 聲明
  • address
  • phone,用於請求 phone_numberphone_number_verified 聲明

儘管Spring目前尚不支持此功能,但規範允許通過在授權請求中指定單個聲明來請求單個聲明。

6. Spring 對 OIDC 發現的支持

正如我們在引言中所解釋的,OIDC 包含許多除了其核心目的之外的不同功能。

本節以及後續章節所分析的功能在 OIDC 中是可選的。因此,重要的是要理解可能存在不支持這些功能的 OP。

規範定義了一種發現機制,允許 RP 發現 OP 並獲取與其交互所需的信息。

簡而言之,OP 提供一個標準的元數據 JSON 文檔。該信息必須通過發行者位置的已知端點提供,即/.well-known/openid-configuration

Spring 能夠從中受益,通過僅使用一個簡單的屬性(發行者位置)來配置ClientRegistration

但讓我們直接來看一個示例以更清楚地説明這一點。

我們將定義一個自定義ClientRegistration實例:

spring:
  security:
    oauth2:
      client:
        registration: 
          custom-google: 
            client-id: <client-id>
            client-secret: <secret>
        provider:
          custom-google:
            issuer-uri: https://accounts.google.com

現在我們可以重啓我們的應用程序,並檢查日誌以確認應用程序在啓動過程中是否正在調用 openid-configuration 端點。

我們甚至可以瀏覽此端點,以查看 Google 提供的信息:

https://accounts.google.com/.well-known/openid-configuration

我們可以看到,例如,服務需要使用的 Authorization、Token 和 UserInfo 端點,以及支持的 scope。

特別需要注意的是,如果啓動時 Discovery 端點不可用,我們的應用程序將無法成功完成啓動過程。

7. OpenID Connect 會話管理

本規範補充了核心功能,定義了以下內容:

  • 不同方式持續監控最終用户的登錄狀態,以便 RP 可以註銷因已從 OpenID 提供者註銷的最終用户
  • 在客户端註冊時,將 RP 註銷 URI 註冊到 OP,以便在最終用户從 OP 註銷時收到通知
  • 一種機制,通知 OP 最終用户已從網站註銷,並可能也需要從 OP 註銷

自然地,並非所有 OP 都支持所有這些項,並且這些解決方案可能僅通過用户代理在前端實現。

在本教程中,我們將重點關注列表的最後一項,即 RP 主動註銷的能力

此時,如果我們在我們的應用程序中登錄,我們通常可以訪問所有端點。

如果我們在調用 /logout 端點註銷,然後在稍後請求受保護的資源,我們將會看到,無需重新登錄,即可獲得響應。

然而,實際上並非如此。如果我們在瀏覽器調試控制枱中檢查“網絡”選項卡,當我們第二次命中受保護的端點時,我們會被重定向到 OP 授權端點。由於我們仍然在該端點登錄,流程將透明地完成,幾乎立即到達受保護的端點。

當然,這在某些情況下可能不是期望的行為。讓我們看看如何使用此 OIDC 機制來處理這種情況。

7.1. OpenID 提供者配置

在本例中,我們將配置並使用 Okta 實例作為我們的 OpenID 提供者。 我們不會詳細介紹如何創建實例,但可以遵循 此指南 的步驟,同時請注意 Spring Security 的默認回調端點將是 login/oauth2/code/okta</em/>。

在我們的應用程序中,我們可以使用屬性定義客户端註冊數據:

spring:
  security:
    oauth2:
      client:
        registration: 
          okta: 
            client-id: <client-id>
            client-secret: <secret>
        provider:
          okta:
            issuer-uri: https://dev-123.okta.com

OIDC 指出,OP logout 端點可以通過在 Discovery 文檔中指定,作為 end_session_endpoint 元素。

7.2. 退出成功處理程序的配置

接下來,我們需要通過提供自定義的 LogoutSuccessHandler 實例來配置 HttpSecurity 的退出邏輯:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(authorizeRequests -> authorizeRequests
                    .requestMatchers("/home").permitAll()
                    .anyRequest().authenticated())
        .oauth2Login(AbstractAuthenticationFilterConfigurer::permitAll)
        .logout(logout -> logout.logoutSuccessHandler(oidcLogoutSuccessHandler()));
    return http.build();
}

現在讓我們看看如何使用 Spring Security 提供的特殊類 OidcClientInitiatedLogoutSuccessHandler,來為這個目的創建一個 LogoutSuccessHandler

@Autowired
private ClientRegistrationRepository clientRegistrationRepository;

private LogoutSuccessHandler oidcLogoutSuccessHandler() {
    OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler =
      new OidcClientInitiatedLogoutSuccessHandler(
        this.clientRegistrationRepository);

    oidcLogoutSuccessHandler.setPostLogoutRedirectUri(
      URI.create("http://localhost:8081/home"));

    return oidcLogoutSuccessHandler;
}

因此,我們需要將該 URI 設置為有效的註銷重定向 URI,在 OP 客户端配置面板中。

顯然,OP 註銷配置包含在客户端註冊設置中,因為我們所使用的配置處理器的 Bean 是上下文中的 ClientRegistrationRepository Bean。

那麼,接下來會發生什麼呢?

在我們登錄到應用程序後,我們可以向 Spring Security 提供的 /logout 端點發送請求。

如果在瀏覽器調試控制枱中檢查網絡日誌,您會看到我們首先被重定向到 OP 註銷端點,然後再訪問我們配置的重定向 URI。

下次我們訪問應用程序中需要身份驗證的端點時,我們必須在 OP 平台上再次登錄以獲取權限。

8. 結論

總而言之,本文介紹了OpenID Connect提供的解決方案以及如何使用Spring Security實施其中一些方案。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.