[英]Springboot testing with keycloak
我正在嘗試運行簡單的單元測試,以正確的方式配置了 Keycloak(我對其進行了測試,並且我的 mvc 應用程序正在連接並且用戶已通過身份驗證_ 但現在我嘗試測試我的控制器,即使我使用了 spring slices keycloak 適配器被調用和給我錯誤。適配器配置主要來自 keycloak 文檔
@WebMvcTest(UserController.class)
class UserControllerTest {
@MockBean
UserService userService;
@Autowired
MockMvc mockMvc;
@BeforeEach
void setUp() {
}
@AfterEach
void tearDown() {
reset(userService);
}
@Test
void logout() throws Exception {
mockMvc.perform(get("/logout"))
.andExpect(status().isOk());
}
}
但是當我嘗試運行它時出現錯誤,堆棧跟蹤:
java.lang.NullPointerException
at org.keycloak.adapters.KeycloakDeploymentBuilder.internalBuild(KeycloakDeploymentBuilder.java:57)
at org.keycloak.adapters.KeycloakDeploymentBuilder.build(KeycloakDeploymentBuilder.java:205)
at org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver.resolve(KeycloakSpringBootConfigResolver.java:37)
at org.keycloak.adapters.springsecurity.config.KeycloakSpringConfigResolverWrapper.resolve(KeycloakSpringConfigResolverWrapper.java:40)
at org.keycloak.adapters.AdapterDeploymentContext.resolveDeployment(AdapterDeploymentContext.java:89)
at org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter.doFilter(KeycloakPreAuthActionsFilter.java:81)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:74)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:105)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:56)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:215)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:178)
at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:133)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118)
at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:133)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:92)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118)
at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:133)
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118)
at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:133)
at org.springframework.test.web.servlet.MockMvc.perform(MockMvc.java:182)
at org.czekalski.userkeycloak.controller.UserControllerTest.logout(UserControllerTest.java:50)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:532)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:115)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:171)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:72)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:167)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:114)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:59)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$4(NodeTestTask.java:108)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:72)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:98)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:74)
at java.util.ArrayList.forEach(ArrayList.java:1257)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$4(NodeTestTask.java:112)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:72)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:98)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:74)
at java.util.ArrayList.forEach(ArrayList.java:1257)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$4(NodeTestTask.java:112)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:72)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:98)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:74)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:220)
at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$6(DefaultLauncher.java:188)
at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:202)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:181)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:128)
at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:74)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
添加到測試@TestPropertySource("classpath:secTest.properties")
里面 secTest.properties keycloak.enabled = false
沒有幫助
被測代碼:
@Controller
public class UserController {
private final UserService userService;
public UserController( UserService userService) {
this.userService = userService;
}
@GetMapping("/index")
public String logout(){
return "users/logout";
}
@GetMapping("/logged")
public String loggedIn(Model model){
model.addAttribute("token", userService.getloggedInUser());
return "users/logged";
}
我剛剛編寫了一組庫來簡化安全 Spring 應用程序的單元測試。
它包括一個@WithMockKeycloackAuth
注釋,以及 Keycloak 專用的MockMvc
請求后處理器和WebTestClient
配置器/修改器
示例用法:
@RunWith(SpringRunner.class)
@WebMvcTest(GreetingController.class)
@ContextConfiguration(classes = GreetingApp.class)
@ComponentScan(basePackageClasses = { KeycloakSecurityComponents.class, KeycloakSpringBootConfigResolver.class })
public class GreetingControllerTests extends ServletUnitTestingSupport {
@MockBean
MessageService messageService;
@Test
@WithMockKeycloackAuth
public void whenUserIsNotGrantedWithAuthorizedPersonelThenSecretRouteIsNotAccessible() throws Exception {
mockMvc().get("/secured-route").andExpect(status().isForbidden());
}
@Test
@WithMockKeycloackAuth("AUTHORIZED_PERSONNEL")
public void whenUserIsGrantedWithAuthorizedPersonelThenSecretRouteIsAccessible() throws Exception {
mockMvc().get("/secured-route").andExpect(content().string(is("secret route")));
}
@Test
@WithMockKeycloakAuth(
authorities = { "USER", "AUTHORIZED_PERSONNEL" },
id = @IdTokenClaims(sub = "42"),
oidc = @OidcStandardClaims(
email = "ch4mp@c4-soft.com",
emailVerified = true,
nickName = "Tonton-Pirate",
preferredUsername = "ch4mpy"),
privateClaims = @ClaimSet(stringClaims = @StringClaim(name = "foo", value = "bar")))
public void whenAuthenticatedWithKeycloakAuthenticationTokenThenCanGreet() throws Exception {
mockMvc().get("/greet")
.andExpect(status().isOk())
.andExpect(content().string(startsWith("Hello ch4mpy! You are granted with ")))
.andExpect(content().string(containsString("AUTHORIZED_PERSONNEL")))
.andExpect(content().string(containsString("USER")));
根據我建議的工具數量,您可能會從 maven-central 獲得spring-security-oauth2-test-addons
或spring-security-oauth2-test-webmvc-addons
:
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-security-oauth2-test-addons</artifactId>
<version>2.3.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-security-oauth2-test-webmvc-addons</artifactId>
<version>2.3.4</version>
<scope>test</scope>
</dependency>
如果您只對@WithMockKeycloakAuth
注釋感興趣,首先就足夠了。 第二個添加了流暢的 API(MockMvc 請求后處理器)和其他東西,比如 MockMvc 包裝器,內容類型和接受標頭的默認值
我也找到了一種方法來做到這一點,但它的方法很丑陋。 您可以出於測試目的關閉keycloak。 可以更好嗎?
在屬性文件(我的是 app-dev.properties)中設置:
keycloak.enabled = false
在我設置的應用程序安全配置類中
@ConditionalOnProperty(value = "keycloak.enabled", matchIfMissing = true)
public class KeycloakConfiguration extends KeycloakWebSecurityConfigurerAdapter {
我還使用安全配置創建了單獨的類,但僅用於使用這些注釋進行測試
@Profile("app-dev.properties")
@Configuration
@EnableWebSecurity
public class TestSecConfig extends WebSecurityConfigurerAdapter{}
在控制器的集成測試中
@ActiveProfiles("app-dev.properties")
@WebMvcTest(value = FunController.class)
@Import(TestSecConfig.class)
@TestPropertySource("classpath:app-dev.properties")
class FunControllerIT{}
來源:
解決方法https://github.com/spring-projects/spring-boot/issues/6514
測試期間 Keycloak + Spring Security 設置的解決方案很棘手,但恕我直言,以下只是測試正確設置環境的正確解決方案。 首先,我們不想有條件地使用安全配置,因為我們也想測試它(例如,RolesAllowed、Post 和 Pre 注釋)。 出於同樣的原因,我們不想為測試創建特殊的配置。 出路是如下配置:
@Configuration #mandatory
@EnableWebSecurity #mandatory
@EnableGlobalMethodSecurity(jsr250Enabled = true) #conditional
@EnableConfigurationProperties(KeycloakSpringBootProperties.class) #mandatory
@Slf4j #conditional
class WebSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Override
protected void configure(@NotNull HttpSecurity http) throws Exception {
super.configure(http);
...
}
@Bean
public @NotNull KeycloakConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
}
真正重要的是@EnableConfigurationProperties(KeycloakSpringBootProperties.class)
的存在。 沒有它,您將在測試期間獲得 NPE。 在測試資源中的 application.yml 或 application-test.yml(屬性配置也是如此)添加以下內容:
keycloak:
enabled: false #Keycloak is not needed in full functionality
realm: mock #There is no configuration mock for Keycloak in case of testing. Realm must be set but it is not used
resource: mock #There is no configuration mock for Keycloak in case of testing. Resource must be set but it is not used
auth-server-url: http://mock #There is no configuration mock for Keycloak in case of testing. URL must be set but it is not used
bearer-only: true # Because Keycloak do redirect in case of unauthenticated user which leads to 302 status, we switch to strict Bearer mode
credentials:
secret: mock
使用此設置和@WithMockUser
注釋,您的@WebMvcTest
將在與生產相同的安全配置中運行,不會出現錯誤。
您還可以使用@AutoConfigureMockMvc(addFilters = false)
注釋您的測試@AutoConfigureMockMvc(addFilters = false)
以禁用應用程序上下文中的過濾器。
另一個角度是像測試服務一樣測試你的控制器(沒有 @WebMvcTest 注釋)。 畢竟我們真的不需要測試 Spring 路由 GET 和 POST 以及返回 HTTP 響應的功能。 Spring 自己對此進行了測試。 我們只關心測試我們編寫的工作單元......控制器功能本身的核心。
@ExtendWith(UserController.class) class UserControllerTest { @Mock UserService userService; @Test void logoutShouldShowUsersLogout() throws Exception { var result = logout(); assertEquals(result, "users/logout"); } }
只是一個想法。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.