我想为我的弹簧控制器编写单元测试。我正在使用 keycloak 的 openid 流程来保护我的端点。
在我的测试中,我使用
@WithMockUser
注释来模拟经过身份验证的用户。我的问题是我正在从主体的令牌中读取 userId 。我的单元测试现在失败了,因为我从令牌中读取的userId
为空;
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
...或...
拿一个“现成的”,就像我的这个lib一样,可以从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 = "[email protected]",
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流畅的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
}