在 Spring 中偽造 OAuth2 單一登入
1.概述
我們經常需要在應用程式中實作 OAuth2 單一登入。有了它,用戶只需登入一次即可訪問其他應用程序,而無需反覆登入。通常,它有一個授權伺服器來管理身份驗證部分,而這通常是第三方的。
在這種情況下進行測試會變得很困難。為了解決這個問題,我們需要模擬授權伺服器。在本教程中,我們將學習兩種在 Spring 應用中模擬和繞過 OAuth2 SSO 的方法。
首先,我們將建立一個簡單的 Spring Boot 應用程序,啟用 OAuth2 SSO,並使用 Keycloak 作為授權伺服器。然後,我們將學習兩種使用測試案例來偽造 OAuth2 SSO 的方法。
2. Spring App 中的 OAuth2
在本節中,我們將建立一個小型的、簡單的 Spring Boot 應用程序,並使用 OAuth2 SSO,其中授權伺服器為 Keycloak。本節內容會比較簡短,因為我們的主要目標是學習如何偽造 OAuth2 SSO。
讓我們從[start.spring.io](https://start.spring.io)
建立一個具有以下相依性的 Spring Boot 應用程式:
-
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 僅傳回一則訊息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 來驗證除login
、 oauth2
、 error
等端點之外的每個請求。登入類型為 OAuth2。 successHandler 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-grant-type 設定為authorization-code
。
它使用佔位符動態建構重定向 URI,使應用程式能夠在身份驗證後正確處理 OAuth2 回調。 provider 下的provider
issuer-uri
指向 Keycloak 域my-realm
,這對於發現 Keycloak 的 OAuth2 端點至關重要。
我們可以按照此文件將 Keycloak 配置為授權伺服器。配置 Keycloak 時,我們只需匹配client-id
和issuer-url
。雖然測試不需要這樣做,但我們可以嘗試存取 API 並查看身份驗證的實際效果。
3. 偽造 OAuth2 SSO
在本節中,我們將學習兩種偽造 OAuth2 SSO 的方法。一種方法是完全繞過身份驗證,第二種方法是模擬授權伺服器。在這兩種情況下,我們都不需要在執行測試案例時執行 Keycloak。
3.1. 使用MockMvc
繞過身份驗證
為了繞過身份驗證,我們需要一個虛擬提供程序,該提供程序可以透過OAuth2AuthorizedClientService
bean 註冊為客戶端.
為此,讓我們為該虛擬提供者建立一個測試配置:
@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());
}
}
這裡我們在本次Spring Boot測試中自動配置MockMvc
並導入NoOAuthConfig
配置類別。
oauth2login()
是SecurityMockMvcRequestPostProcessors
的一部分,它會建立一個SecurityContext
,其中包含用於身份驗證OAuth2AuthenticationToken
、作為主體的OAuth2User
以及會話中的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,
這是一個 JSON 文件,由 OpenID Connect 提供者透過 HTTPS 以眾所周知的 URL 提供,它提供了客戶端與其整合所需的所有設定詳細資訊。它允許客戶端應用程式(例如 Web 或行動應用程式)自動發現身份驗證端點、令牌端點、支援的功能、公鑰、作用域、聲明等。
讓我們加入程式碼來模擬授權伺服器中的端點:
static WireMockServer wireMockServer;
@BeforeAll
static void setup() {
wireMockServer = new WireMockServer(8080);
configureFor(8080);
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) 來攔截對my-realm
域下/.well-known/openid-configuration
的請求。該存根會傳回一個模擬 JSON 回應,該回應模仿真實的 Keycloak 配置,包括授權、代幣交換和使用者資訊的端點。
此設定使應用程式在測試期間相信它正在與真實的 Keycloak 伺服器互動。最後, tearDown()
會在所有測試完成後停止 WireMock 伺服器。
現在,讓我們來編寫測試案例:
@Test
void whenAuthServerIsMocked_thenItShouldBeAbleToLogin() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/")
.with(oauth2Login()))
.andExpect(status().isOk());
}
4. 結論
在本文中,我們學習如何使用 Keycloak 作為授權伺服器來設定基本的 OAuth2 單一登入 (SSO)。然後,我們探討如何在編寫測試案例時模擬身份驗證。
第一種方法,我們僅使用 Spring Security 配置來繞過身份驗證。第二種方法,我們使用 WireMock API 來模擬授權伺服器,而無需執行實際的驗證伺服器。
本文使用的所有程式碼範例均可在 GitHub 上找到。