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

Spring Security
Remote
1
11:26 AM · Nov 30 ,2025

1. 概述

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

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

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

2. OAuth2 in Spring App

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

讓我們創建一個 Spring Boot 應用程序,如下所示的依賴項來自start.spring.io

現在,讓我們創建一個 REST 端點資源,並且它將被保護。沒有人可以訪問這些未經身份驗證:

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

此 API 簡單地返回消息 Login Success。現在,讓我們學習如何使用 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。

現在,讓我們為 OAuth2 和授權服務器添加一些屬性,這些屬性位於 application.yaml 文件中。 授權服務器在 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-uriprovider 下指向 Keycloak 領域 my-realm,這對於發現 Keycloak 的 OAuth2 端點至關重要。

我們可以通過遵循此 文檔 來配置 Keycloak 作為授權服務器。 我們只需要匹配 client-idissuer-url

3. Faking the OAuth2 SSO

In this section, we’ll learn two ways to fake OAuth2 SSO. One way is to bypass the authentication altogether, and the second way is to mock the authorization server. In both scenarios, we don’t need to run Keycloak while running our test cases.

3.1. Bypassing Authentication With MockMvc

To bypass the authentication, we need a dummy provider that can be registered as a client by the OAuth2AuthorizedClientService bean. To do that, let’s create a test configuration for this dummy provider:

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

The above test configuration creates a client and registers it with the OAuth2AuthorizedClientService. If we look carefully at clientRegistrationRepository(), it has all the properties Spring needs to register clients. We can verify with the properties in the application.yaml file from earlier.

Now, let’s write the test case:

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

Here, we auto-configure MockMvc and import the NoOAuthConfig configuration class in this Spring Boot test.

The oauth2login() is part of SecurityMockMvcRequestPostProcessors and establishes a SecurityContext that has an OAuth2AuthenticationToken for the Authentication, an OAuth2User as the principal, and an OAuth2AuthorizedClient in the session. This is how we bypass the authentication.

3.2. Fake an OAuth2 SSO Service With WireMock

To mock the authorization server, we’ll use WireMock:

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

The latest version of WireMock is available in the Maven Repository.

Now, let’s add the following configuration in the application-test.yaml file, which is similar to the main application.yaml file content:

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

To mock the authorization server, we’ll mock one API /.well-known/openid-configuration, a JSON file that an OpenID Connect provider serves over HTTPS at a well-known URL, providing all the configuration details a client needs to integrate with it. It allows client applications (like web or mobile apps) to automatically discover auth endpoints, token endpoints, supported features, public keys, scopes, claims, etc.

Let’s add the code to mock the endpoint in the authorization server:

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

Here, we set up a WireMock server to mock Keycloak for testing OAuth2 login flows. It runs on port 8787 and define a stub to intercept requests to /.well-known/openid-configuration under the my-realm realm. The stub returns a mock JSON response that mimics a real Keycloak configuration, including endpoints for authorization, token exchange, and user info.

This setup makes the application believe it’s interacting with a real Keycloak server during tests. Finally, the tearDown() stops the WireMock server after all tests are complete.

Now, let’s write the test case:

@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.