使用 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-addonsspring-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

SpringBoot + Keycloak 适配器设置失败

使用 Keycloak 和 SpringBoot 的多租户

无法使用 keycloak 构建基于 spring 的项目进行身份验证

Spring Boot 我无法切换 keycloak 和基本身份验证

springboot 应用程序中的 keycloak.json 文件