使用 keycloak 进行 Springboot 测试
Posted
技术标签:
【中文标题】使用 keycloak 进行 Springboot 测试【英文标题】:Springboot testing with keycloak 【发布时间】:2020-01-17 12:19:20 【问题描述】:我正在尝试运行简单的单元测试,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")
where inside 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配置? 你这是什么意思? 您找到解决方案了吗?我也有同样的问题。 【参考方案1】:我刚刚写了一组库to ease unit-testing of secured Spring apps。
它包括一个@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 包装器,具有默认值的内容类型和接受标头
【讨论】:
好吧,干得好!那时我以不同的方式解决了它。解决方案发布在单独的线程中。我还必须测试您的解决方案:) 在版本2.3.4
中没有注释WithMockKeycloakAuth
而是WithMockAuthentication
。还有一点,我也没有找到mockMvc()
这个方法。有什么建议吗?
@WithMockKeycloakAuth 在 2.3.4 中有:github.com/ch4mpy/spring-addons/blob/master/…
maven 依赖 sn-p 中有错字。对不起。我的库仅支持带有 MockMvc 的 KeycloakAuthenticationToken(不是 WebTestClient),因此应该将 spring-security-oauth2-test-webmvc-addons 而不是 spring-security-oauth2-test-webflux-addons 作为测试依赖项拉
最后的建议,请阅读自述文件,特别是关于在本地克隆 repo 以实验测试样本的部分。您将节省大量时间来配置自己的测试。【参考方案2】:
我也找到了一种方法来做到这一点,但它是相当丑陋的方式。您可以出于测试目的关闭 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
【讨论】:
【参考方案3】:您还可以使用 @AutoConfigureMockMvc(addFilters = false)
注释您的 Testclass 以禁用应用程序上下文中的过滤器。
【讨论】:
【参考方案4】:测试期间 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
将在与生产环境相同的安全配置中运行而不会出现错误。
【讨论】:
在尝试了很多事情之后,它工作正常!以上是关于使用 keycloak 进行 Springboot 测试的主要内容,如果未能解决你的问题,请参考以下文章
Keycloak 3.4.3 和 springboot 2.0
无法使用 keycloak 构建基于 spring 的项目进行身份验证