模拟用于测试 Spring 控制器的 Keycloak 令牌



我想为我的弹簧控制器编写单元测试。我正在使用keycloak的openid流程来保护我的端点。

在我的测试中,我使用 @WithMockUser 注释来模拟经过身份验证的用户。我的问题是我正在从主体的令牌中读取 userId。我的单元测试现在失败,因为我从令牌读取的userId为 null;

        if (principal instanceof KeycloakAuthenticationToken) {
            KeycloakAuthenticationToken authenticationToken = (KeycloakAuthenticationToken) principal;
            SimpleKeycloakAccount account = (SimpleKeycloakAccount) authenticationToken.getDetails();
            RefreshableKeycloakSecurityContext keycloakSecurityContext = account.getKeycloakSecurityContext();
            AccessToken token = keycloakSecurityContext.getToken();
            Map<String, Object> otherClaims = token.getOtherClaims();
            userId = otherClaims.get("userId").toString();
        }

有什么可以轻易嘲笑KeycloakAuthenticationToken

的吗?
@WithmockUser使用

UsernamePasswordAuthenticationToken配置安全上下文。这对于大多数用例来说都很好,但是当您的应用依赖于另一个身份验证实现(就像您的代码一样(时,您必须构建或模拟正确类型的实例并将其置于测试安全上下文中:SecurityContextHolder.getContext().setAuthentication(authentication);

当然,您很快就会想要自动化此操作,构建自己的注释或RequestPostProcessor

。或。。。

拿一个"现成的",就像我的这个库一样,可以从Maven-Central获得:

<dependency>
    <!-- just enough for @WithMockKeycloackAuth -->
    <groupId>com.c4-soft.springaddons</groupId>
    <artifactId>spring-security-oauth2-test-addons</artifactId>
    <version>3.0.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <!-- required only for WebMvc "fluent" API -->
    <groupId>com.c4-soft.springaddons</groupId>
    <artifactId>spring-security-oauth2-test-webmvc-addons</artifactId>
    <version>3.0.1</version>
    <scope>test</scope>
</dependency>

您可以将其与@WithMockKeycloackAuth注释一起使用:

@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("TESTER")
    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" },
            claims = @OpenIdClaims(
                    sub = "42",
                    email = "ch4mp@c4-soft.com",
                    emailVerified = true,
                    nickName = "Tonton-Pirate",
                    preferredUsername = "ch4mpy",
                    otherClaims = @Claims(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")));
    }

或 MockMvc fluent API (RequestPostProcessor(:

@RunWith(SpringRunner.class)
@WebMvcTest(GreetingController.class)
@ContextConfiguration(classes = GreetingApp.class)
@ComponentScan(basePackageClasses = { KeycloakSecurityComponents.class, KeycloakSpringBootConfigResolver.class })
public class GreetingControllerTest extends ServletKeycloakAuthUnitTestingSupport {
    @MockBean
    MessageService messageService;
    @Test
    public void whenUserIsNotGrantedWithAuthorizedPersonelThenSecretMethodIsNotAccessible() throws Exception {
        mockMvc().with(authentication().roles("TESTER")).get("/secured-method").andExpect(status().isForbidden());
    }
    @Test
    public void whenUserIsGrantedWithAuthorizedPersonelThenSecretMethodIsAccessible() throws Exception {
        mockMvc().with(authentication().roles("AUTHORIZED_PERSONNEL")).get("/secured-method")
                .andExpect(content().string(is("secret method")));
    }
}

我不喜欢添加额外的依赖项,特别是当它仅与测试用例场景相关时。此外,在某些项目中添加依赖项是一个需要安全检查的大过程,需要得到许多经理、高级管理人员等的批准。所以这是我的解决方案,它允许模拟Keycloak安全上下文,而无需keycloak和其他额外依赖项的实例。这是从我的项目中复制的,因此需要进行调整。希望对您有所帮助。

@Test
    void shouldFooOnProtectedEndpoint() throws Exception {
        //given
        AccessToken token = new AccessToken();
        
        // by username i was differentiate is it allowed
        token.setPreferredUsername(SUBMITTER_USERNAME);
        KeycloakSecurityContext keycloakSecurityContext = mock(KeycloakSecurityContext.class);
        given(keycloakSecurityContext.getToken()).willReturn(token);
        KeycloakPrincipal principal = mock(KeycloakPrincipal.class);
        given(principal.getKeycloakSecurityContext()).willReturn(keycloakSecurityContext);
        Authentication auth = mock(Authentication.class);
        given(auth.getPrincipal()).willReturn(principal);
        SecurityContextHolder.getContext().setAuthentication(auth);
        ... test logic
}

最新更新