Spring Security 與 OpenID Connect

Spring Security
Remote
1
11:18 PM · Nov 29 ,2025

注意:本文已更新至新的 Spring Security OAuth 2.0 堆棧。儘管仍然可以使用舊的堆棧進行教程,但該教程仍然可用。

注意:本文已更新至新的 Spring Security OAuth 2.0 堆棧。儘管仍然可以使用舊的堆棧進行教程,但該教程仍然可用。

1. 概述

在本教程中,我們將重點介紹使用 Spring Security 設置 OpenID Connect (OIDC) 的方法。

我們將介紹該規範的不同方面,然後我們將看到 Spring Security 如何支持在 OAuth 2.0 客户端上實現它。

2. 快速 OpenID Connect 簡介

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

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

OIDC 規範套件非常廣泛,包括核心功能和多個可選能力,這些能力以不同的組進行呈現。 以下是主要內容:

  • 核心 – 身份驗證和使用聲明來溝通端用户信息
  • 發現 – 規定客户端如何動態確定關於 OpenID 提供者(OP)的信息
  • 動態註冊 – 規定客户端如何與提供者註冊
  • 會話管理 – 定義如何管理 OIDC 會話

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

此外,客户端可以通過在授權請求中添加 openid  範圍來請求使用此擴展。

最後,對於本教程,有助要知道,OP 會以名為 ID 令牌的 JWT 形式發出端用户信息。

現在,我們準備好深入瞭解 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>

這個 starter 構件聚合了所有 Spring Security Client 相關依賴項,包括

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

正如往常一樣,我們可以使用 Maven Central 搜索引擎 找到這個構件的最新版本。

4. 基本配置使用 Spring Boot

首先,我們將配置應用程序使用我們剛剛創建的與 Google 集成的客户端註冊信息。

使用 Spring Boot 使這非常容易,因為我們只需要定義兩個應用程序屬性即可:

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

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

這看起來很簡單,但實際上這裏有很多事情在發生。接下來,我們將探索 Spring Security 如何完成這項工作。

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

在那裏,我們看到除了客户端 ID 和客户端密鑰之外,還需要提供額外的數據才能成功配置 ClientRegistration 實例。

所以,這如何運作的呢?

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

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

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

  • 默認的範圍將使用
  • 授權端點
  • 令牌端點
  • 用户信息端點,這也是 OIDC 核心規範的一部分

4.1. 訪問用户信息

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

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

  • 檢索 ID 令牌值和包含的聲明
  • 獲取來自用户信息端點的聲明
  • 生成兩個集合的聚合

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

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

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

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

如果我們檢查 principal,我們會看到這裏有很多有用的信息,例如用户的姓名、電子郵件、個人資料圖片和區域設置。

5. OIDC in Action

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

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

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

因此,讓我們來了解一下幕後發生的事情,以便更好地理解規範如何得以實現,並最大限度地利用它。

5.1. The Login Process

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

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

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

存在一些差異。

First, depending on the provider we’re using and the scopes we’ve configured, we might see that the service is making a call to the UserInfo endpoint we mentioned at the beginning.

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

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

This means that all the information we’re obtaining is part of the ID Token.

我們可以通過創建並提供自己的 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 的調用。正如我們在 JWS 和 JWK 博文中所解釋的,它用於驗證 JWT 格式的 ID Token 簽名。

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

5.2. The ID Token

當然,OIDC 規範涵蓋並適應了各種場景。In this case, we’re using the Authorization Code flow, and the protocol indicates that both the Access Token and the ID Token will be retrieved as part of the Token Endpoint response.

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

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

我們可以看到 ID Token 包含一些強制聲明:

  • The issuer identifier formatted as a URL (e.g., “https://accounts.google.com“)
  • A subject id, which is a reference of the End User contained by the issuer
  • The expiration time for the token
  • Time at which the token was issued
  • The audience, which will contain the OAuth 2.0 Client ID we’ve configured

它還包含許多 OIDC 標準聲明,例如之前提到的 namelocalepictureemail

As these are standard, we can expect many providers to retrieve at least some of these fields and therefore facilitate the development of simpler solutions.

5.3. Claims and Scopes

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

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

  • profile, which can be used to request default profile Claims (e.g., name, preferred_usernamepicture, etc.)
  • email, to access to the email and email_verified Claims
  • address
  • phone, to request the phone_number and phone_number_verified Claims

儘管 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

我們可以看到,例如,服務需要使用的授權、令牌和用户信息端點,以及支持的範圍。

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

7. OpenID Connect Session Management

This specification complements the Core functionality by defining the following:

  • Different ways to monitor the End User’s login status at the OP on an ongoing basis so that the RP can log out an End User who has logged out of the OpenID Provider
  • The possibility of registering RP logout URIs with the OP as part of the Client registration, in order to be notified when the End User logs out of the OP
  • A mechanism to notify the OP that the End User has logged out of the site and might want to log out of the OP as well

Naturally, not all OPs support all of these items, and some of these solutions can be implemented only in a front-end implementation via the User-Agent.

In this tutorial, we’ll focus on the capabilities offered by Spring for the last item of the list, RP-initiated Logout.

At this point, if we log in to our application, we can normally access every endpoint.

If we log out (calling the /logout endpoint) and we make a request to a secured resource afterward, we’ll see that we can get the response without having to log in again.

However, this is actually not true. If we inspect the Network tab in the browser debug console, we’ll see that when we hit the secured endpoint the second time, we get redirected to the OP Authorization Endpoint. And since we’re still logged in there, the flow is completed transparently, ending up in the secured endpoint almost instantly.

Of course, this might not be the desired behavior in some cases. Let’s see how we can implement this OIDC mechanism to deal with this.

7.1. The OpenID Provider Configuration

In this case, we’ll be configuring and using an Okta instance as our OpenID Provider. We won’t go into details on how to create the instance, but we can follow the steps of this guide, keeping in mind that Spring Security’s default callback endpoint will be /login/oauth2/code/okta.

In our application, we can define the client registration data with properties:

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

OIDC indicates that the OP logout endpoint can be specified in the Discovery document, as the end_session_endpoint element.

7.2. The LogoutSuccessHandler Configuration

Next, we’ll have to configure the HttpSecurity logout logic by providing a customized LogoutSuccessHandler instance:

@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();
}

Now let’s see how we can create a LogoutSuccessHandler for this purpose using a special class provided by Spring Security, the OidcClientInitiatedLogoutSuccessHandler:

@Autowired
private ClientRegistrationRepository clientRegistrationRepository;

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

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

    return oidcLogoutSuccessHandler;
}

Consequently, we’ll need to set up this URI as a valid logout Redirect URI in the OP Client configuration panel.

Clearly, the OP logout configuration is contained in the client registration setup since all we’re using to configure the handler is the ClientRegistrationRepository bean present in the context.

So, what will happen now?

After we log in to our application, we can send a request to the /logout endpoint provided by Spring Security.

If we check the Network logs in the browser debug console, we’ll see we got redirected to an OP logout endpoint before finally accessing the Redirect URI we configured.

Next time we access an endpoint in our application that requires authentication, we’ll mandatorily need to log in again in our OP platform to get permissions.

8. 結論

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

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

發佈 評論

Some HTML is okay.