知識庫 / Spring / Spring Security RSS 訂閱

模擬 OAuth2 單點登錄在 Spring 中的實現

Spring Security
HongKong
11
10:46 AM · Dec 06 ,2025

1. 概述

我們經常需要在應用程序中實現 OAuth2 單點登錄 (SSO)。 這樣,用户一旦登錄一次,就可以在不重複登錄的情況下訪問其他應用程序。 通常,它有一個授權服務器來管理身份驗證部分,並且這個授權服務器通常由第三方提供。

在這樣的情況下,測試變得困難。 為了克服這個問題,我們需要模擬授權服務器。 在本教程中,我們將學習兩種方法來模擬和繞過 Spring 應用中的 OAuth2 SSO。

首先,我們將創建一個簡單的 Spring Boot 應用程序,其中啓用了 OAuth2 SSO,並使用 Keycloak 作為授權服務器。 然後,我們將學習兩種方法,通過測試用例來模擬 OAuth2 SSO。

2. OAuth2 在 Spring 應用中的應用

在本節中,我們將創建一個簡單的 Spring Boot 應用,其中包含 OAuth2 SSO 功能,授權服務器將使用 Keycloak。 我們將保持本節簡短,因為我們的主要目標是學習如何模擬 OAuth2 SSO。

讓我們創建一個 Spring Boot 應用,並添加以下依賴項(從 start.spring.io):

現在,讓我們創建一個 REST 端點資源,並且該資源將被保護。 任何未經身份驗證的人都無法訪問這些資源。

@GetMapping("/")
public String get() {
    return "Login Success";
}

此API僅返回一個消息“登錄成功”。現在,讓我們學習如何使用OAuth2配置來安全地保護應用程序。

2.1. 配置 OAuth2

現在,讓我們學習如何使用 OAuth2 設置配置應用程序。

首先,讓我們創建一個用於處理安全性和 OAuth2 相關配置的類:

@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
          .authorizeHttpRequests(a ->
              a.requestMatchers(
                new AntPathRequestMatcher("/login"),
                new AntPathRequestMatcher("/oauth2/**"),
                new AntPathRequestMatcher("/openid-connect"),
                new AntPathRequestMatcher("/error"),
                new AntPathRequestMatcher("/css/**"),
                new AntPathRequestMatcher("/js/**"),
                new AntPathRequestMatcher("/images/**"),
                new AntPathRequestMatcher("/assets/**"))
              .permitAll()
              .anyRequest().authenticated())
              .oauth2Login(customizer -> customizer.successHandler(successHandler()))
              .build();
    }

    public AuthenticationSuccessHandler successHandler() {
        SimpleUrlAuthenticationSuccessHandler handler = new SimpleUrlAuthenticationSuccessHandler();
        handler.setDefaultTargetUrl("/");
        return handler;
    }
}

在這裏,我們配置SecurityFilterChain Bean,以驗證所有請求,除了某些端點,如loginoauth2error等。登錄類型為 OAuth2。

successHandler() 確保在成功登錄後,將流程重定向到應用程序的 REST 端點,並返回消息 Login Success. 

現在,讓我們在 application.yaml 文件中添加與 OAuth2 和授權服務器相關的屬性。授權服務器在 application.yaml 文件中稱為 provider

spring:
    security:
        oauth2:
            client:
                registration:
                    keycloak:
                        client-id: my-client
                        scope: openid,profile,email
                        authorization-grant-type: authorization_code
                        redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
                provider:
                     keycloak:
                         issuer-uri: http://localhost:8787/realms/my-realm

在這裏,我們配置了 Spring Boot 應用作為授權服務器。我們為 Keycloak 註冊一個客户端 my-client, 併為其指定了 openid, profileemail 範圍,從而啓用基於 OpenID Connect 的身份驗證。配置將授權類型設置為 authorization-code

它動態構建了重定向 URI,使用佔位符,從而使應用程序能夠在身份驗證後正確處理 OAuth2 回調。在 issuer-uri 下,位於 provider 字段下的內容指向 Keycloak 領域 my-realm,這對於發現 Keycloak 的 OAuth2 端點至關重要。

遵循此 文檔,您可以配置 Keycloak 作為授權服務器。您只需匹配 client-idissuer-url

3. 模擬 OAuth2 單點登錄 (SSO)

本節將學習兩種模擬 OAuth2 單點登錄 (SSO) 的方法。一種方法是完全繞過身份驗證,另一種方法是模擬授權服務器。在兩種情況下,我們無需同時運行 Keycloak,即可運行測試用例。

3.1. 使用 MockMvc 繞過身份驗證

為了繞過身份驗證,我們需要一個模擬提供者,該提供者可以作為 OAuth2AuthorizedClientService 託管的客户端。 要做到這一點,讓我們為該模擬提供者創建一個測試配置:

@TestConfiguration
public class NoOAuth2Config {
    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        ClientRegistration registration = ClientRegistration
          .withRegistrationId("dummy")
          .clientId("test-client")
          .clientSecret("test-secret")
          .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
          .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
          .authorizationUri("http://localhost/fake-auth")
          .tokenUri("http://localhost/fake-token")
          .userInfoUri("http://localhost/fake-userinfo")
          .userNameAttributeName("sub")
          .clientName("Dummy Client")
          .build();
        return new InMemoryClientRegistrationRepository(registration);
    }

    @Bean
    public OAuth2AuthorizedClientService authorizedClientService(
      ClientRegistrationRepository clientRegistrationRepository) {
        return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
    }
}

上述測試配置創建了一個客户端並將其註冊到 OAuth2AuthorizedClientService。如果我們仔細查看 clientRegistrationRepository(),它包含 Spring 所需的所有客户端屬性。我們可以通過之前 application.yaml 文件中的屬性進行驗證。

現在,讓我們編寫測試用例:

@Import(NoOAuth2Config.class)
@ActiveProfiles("test")
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class FakingOauth2SsoIntegrationTest {
    @Autowired
    MockMvc mockMvc;

    @Test
    void whenBypssingTheOAuthWithSpringConfig_thenItShouldBeAbleToLogin() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/")
          .with(oauth2Login()))
          .andExpect(status().isOk());
    }
}

在這裏,我們自動配置了 MockMvc 並導入了該 Spring Boot 測試中的 NoOAuthConfig 配置類。

oauth2login()SecurityMockMvcRequestPostProcessors 的一部分,並建立了一個 SecurityContext,該 SecurityContext 包含一個 OAuth2AuthenticationToken 用於身份驗證,OAuth2User 作為 principal,以及 OAuth2AuthorizedClient 在會話中。 這樣我們就可以繞過身份驗證。

3.2. 使用 WireMock 模擬 OAuth2 SSO 服務

為了模擬授權服務器,我們將使用 WireMock:

<dependency>
    <groupId>com.github.tomakehurst</groupId>
    <artifactId>wiremock-jre8-standalone</artifactId>
    <version>2.35.1</version>
    <scope>test</scope>
</dependency>

最新版本的 WireMock 已在 Maven 倉庫中提供。

現在,讓我們在 application-test.yaml 文件中添加以下配置,該文件與主配置文件 application.yaml 類似:

spring:
    security:
        oauth2:
            client:
                registration:
                    wiremock:
                        client-id: my-client
                        client-secret: my-secret
                        redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
                        authorization-grant-type: authorization_code
                        scope: openid
                        provider: wiremock
                provider:
                    wiremock:
                        issuer-uri: http://localhost:8787/realms/my-realm

為了模擬認證服務器,我們將模擬一個API,即 /.well-known/openid-configuration,這是一個OpenID Connect提供者通過HTTPS在已知URL上提供的JSON文件,其中包含客户端集成的所有配置詳情。它允許客户端應用程序(如Web或移動應用)自動發現認證端點、令牌端點、支持的功能、公鑰、權限範圍、聲明等。

下面是添加模擬該端點的代碼:

static WireMockServer wireMockServer;

@BeforeAll
static void setup() {
    wireMockServer = new WireMockServer(8787);
    configureFor(8787);
    wireMockServer.start();

    stubFor(get(urlEqualTo("/realms/my-realm/.well-known/openid-configuration"))
      .willReturn(aResponse()
      .withHeader("Content-Type", "application/json")
      .withBody("""
            {
                "issuer": "http://localhost:8787/realms/my-realm",
                "authorization_endpoint": "http://localhost:8787/realms/my-realm/oauth/authorize",
                "token_endpoint": "http://localhost:8787/realms/my-realm/oauth/token",
                "userinfo_endpoint": "http://localhost:8787/realms/my-realm/userinfo",
                "jwks_uri": "http://localhost:8787/realms/my-realm/.well-known/jwks.json",
                "response_types_supported": [
                    "code",
                    "token",
                    "id_token",
                    "code token",
                    "code id_token",
                    "token id_token",
                    "code token id_token",
                    "none"
                ],
                "subject_types_supported": [
                    "public"
                ],
                "id_token_signing_alg_values_supported": [
                    "RS256"
                ],
                "scopes_supported": [
                    "openid",
                    "email",
                    "profile"
                ]
            }
        """)));
}

@AfterAll
static void tearDown() {
    wireMockServer.stop();
}

在這裏,我們設置了一個 WireMock 服務器來模擬 Keycloak 以進行 OAuth2 登錄流程的測試。它運行在 8787 端口,並定義一個 stub 來攔截指向 <em >/.well-known/openid-configuration</em> 的請求,該請求位於 <em >my-realm</em> 領域下。該 stub 返回一個模擬真實 Keycloak 配置的 JSON 響應,包括用於授權、令牌交換和用户信息的端點。

這個設置使得應用程序在測試期間認為它正在與真實的 Keycloak 服務器交互。最後,<em >tearDown()</em> 在所有測試完成後停止 WireMock 服務器。

現在,讓我們編寫測試用例:

@Test
void whenAuthServerIsMocked_thenItShouldBeAbleToLogin() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.get("/")
      .with(oauth2Login()))
      .andExpect(status().isOk());
}

4. 結論

在本文中,我們學習瞭如何使用 Keycloak 作為授權服務器設置基本的 OAuth2 單點登錄 (SSO)。我們還探索瞭如何在編寫測試用例時模擬身份驗證。

在第一種方法中,我們僅使用 Spring Security 配置繞過身份驗證。在第二種方法中,我們使用 WireMock API 模擬授權服務器,而無需運行實際的身份驗證服務器。

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

發佈 評論

Some HTML is okay.