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):
- spring-boot-starter-oauth2-client
- spring-boot-starter-security
- spring-boot-starter-web
- spring-boot-starter-test
- spring-security-test
現在,讓我們創建一個 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,以驗證所有請求,除了某些端點,如login、oauth2、error等。登錄類型為 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, profile 和 email 範圍,從而啓用基於 OpenID Connect 的身份驗證。配置將授權類型設置為 authorization-code。
它動態構建了重定向 URI,使用佔位符,從而使應用程序能夠在身份驗證後正確處理 OAuth2 回調。在 issuer-uri 下,位於 provider 字段下的內容指向 Keycloak 領域 my-realm,這對於發現 Keycloak 的 OAuth2 端點至關重要。
遵循此 文檔,您可以配置 Keycloak 作為授權服務器。您只需匹配 client-id 和 issuer-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 模擬授權服務器,而無需運行實際的身份驗證服務器。