@WithUserDetails 似乎不起作用
@WithUserDetails does not seem to work
我有一个应用程序,我在其中使用 Spring 社会保险进行身份验证和授权。不幸的是,我在模拟 Spring 安全方面遇到了一些问题。好像根本没用
我有一个 REST 控制器 returns 404 Not Found 如果它应该 return 的实体标识符不可用。如果用户未登录,则任何页面都会重定向到我的应用程序的社交登录页面。
我已阅读 here @WithUserDetails
注释最适合我。
所以我的测试方法是这样的
@Test
@SqlGroup({
@Sql(executionPhase = ExecutionPhase.BEFORE_TEST_METHOD, statements = "INSERT INTO UserAccount(id, creationtime, modificationtime, version, email, firstname, lastname, role, signinprovider) VALUES (1, '2008-08-08 20:08:08', '2008-08-08 20:08:08', 1, 'user', 'John', 'Doe', 'ROLE_USER', 'FACEBOOK')"), })
@Rollback
@WithUserDetails
public void ifNoTeamsInTheDatabaseThenTheRestControllerShouldReturnNotFoundHttpStatus() {
ResponseEntity<String> response = restTemplate.getForEntity("/getTeamHistory/{team}", String.class, "Team");
Assert.assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
}
但这似乎根本不起作用。看起来测试方法是用匿名用户执行的,因为我得到的状态是 200 OK.
我的测试class是这样注释的
@RunWith(SpringRunner.class)
@ActiveProfiles("dev")
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Transactional
public class TeamRestControllerTest {
//...
}
有没有人遇到过这样的问题,即嘲笑 Spring 由 Spring Social 提供的安全性?
目前我无法对其进行测试,但这里有一个可能的解决方案。
查看@WithUserDetails 实现:
@WithSecurityContext(factory = WithUserDetailsSecurityContextFactory.class)
public @interface WithUserDetails {
...
}
final class WithUserDetailsSecurityContextFactory implements
WithSecurityContextFactory<WithUserDetails> {
private BeanFactory beans;
@Autowired
public WithUserDetailsSecurityContextFactory(BeanFactory beans) {
this.beans = beans;
}
public SecurityContext createSecurityContext(WithUserDetails withUser) {
String beanName = withUser.userDetailsServiceBeanName();
UserDetailsService userDetailsService = StringUtils.hasLength(beanName)
? this.beans.getBean(beanName, UserDetailsService.class)
: this.beans.getBean(UserDetailsService.class);
String username = withUser.value();
Assert.hasLength(username, "value() must be non empty String");
UserDetails principal = userDetailsService.loadUserByUsername(username);
Authentication authentication = new UsernamePasswordAuthenticationToken(
principal, principal.getPassword(), principal.getAuthorities());
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
return context;
}
}
您可以按照相同的模式创建您选择的安全上下文:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@WithSecurityContext(factory = WithoutUserFactory.class)
public @interface WithoutUser {
}
public class WithoutUserFactory implements WithSecurityContextFactory<WithoutUser> {
public SecurityContext createSecurityContext(WithoutUser withoutUser) {
return SecurityContextHolder.createEmptyContext();
}
}
其他可用注释:WithAnonymousUser
、WithMockUser
、WithSecurityContext
(和WithUserDetails
)
添加我的解决方法,可能对其他人有帮助。
我想我遇到了同样的问题:
- A
@Testcontainers
(用于 PostgreSQL 数据库模拟)+ @SpringBootTest
测试。
- 通过注释
@WithSecurityContext
和模拟 factory
模拟了 SecurityContext
。
- 我需要这个模拟 Envers
RevisionListener
,我从 Keycloak 通常创建的 SecurityContext
中获取 userName 和 userId。
- 在测试中调用 Spring bean 时,模拟工作正常。
- 但是当通过
TestRestTemplate
调用 API 时,SecurityContext
没有被模拟,而是为所有字段(主体等)返回 null
。
原来的class是这样的:
@SpringBootTest(
classes = SpringBootInitializer.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = {"keycloak.enabled=false"}
)
@ContextConfiguration(
classes = PersistenceConfiguration.class,
initializers = MyTest.Initializer.class
)
// !!! the SecurityContext mocking will NOT work when calling the controller via REST
@MockKeycloakUser() // do not fail on getting Keycloak data in UserDataRevisionListener
@EnableAutoConfiguration(exclude = { SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class }) // turn off Spring Security to avoid 401 and 302 responses
@Testcontainers // required to fill @Container fields with containers
@Log4j2
@ActiveProfiles("integration-test")
class MyTest {
@Autowired
private TestRestTemplate restTemplate;
// ...
// call via restTemplate looks like this
private List<MyDTO> executeSearchQuery(String query) {
String searchUrl = getSearchUrl(port, query, filter);
MyDTO[] results = this.restTemplate.getForObject(searchUrl, MyDTO[].class);
return List.of(results);
}
// ...
}
我用来使 SecurityContext
工作的是:
- 将
MockMvc
字段添加到测试class。
- 在测试中添加
@AutoConfigureMockMvc
class。
- !!! 通过
MockMvc
而不是 TestRestTemplate
执行 API
看起来像这样:
// all other annotations on the test class stay the same
@AutoConfigureMockMvc // make MockMvc work
// ...
class MyTest {
@Autowired
private MockMvc mockMvc; // trick to make the mock SecurityContext work, which does not work when calling via TestRestTemplate
// Execute the API via mockMvc looks like this:
private String getApiResponse(MyRequest request, int expectedHttpStatus) {
final String url = getRequestUrl();
final String requestBody = JacksonUtils.serializeToString(request);
try {
final MockHttpServletRequestBuilder builder = MockMvcRequestBuilders
.post(url)
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody)
;
// use MockMvc instead of TestRestTemplate to successfully use the mock user emulation
return mockMvc
.perform(builder)
.andExpect(status().is(expectedHttpStatus))
.andReturn()
.getResponse()
.getContentAsString(StandardCharsets.UTF_8);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
// ...
}
我有一个应用程序,我在其中使用 Spring 社会保险进行身份验证和授权。不幸的是,我在模拟 Spring 安全方面遇到了一些问题。好像根本没用
我有一个 REST 控制器 returns 404 Not Found 如果它应该 return 的实体标识符不可用。如果用户未登录,则任何页面都会重定向到我的应用程序的社交登录页面。
我已阅读 here @WithUserDetails
注释最适合我。
所以我的测试方法是这样的
@Test
@SqlGroup({
@Sql(executionPhase = ExecutionPhase.BEFORE_TEST_METHOD, statements = "INSERT INTO UserAccount(id, creationtime, modificationtime, version, email, firstname, lastname, role, signinprovider) VALUES (1, '2008-08-08 20:08:08', '2008-08-08 20:08:08', 1, 'user', 'John', 'Doe', 'ROLE_USER', 'FACEBOOK')"), })
@Rollback
@WithUserDetails
public void ifNoTeamsInTheDatabaseThenTheRestControllerShouldReturnNotFoundHttpStatus() {
ResponseEntity<String> response = restTemplate.getForEntity("/getTeamHistory/{team}", String.class, "Team");
Assert.assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
}
但这似乎根本不起作用。看起来测试方法是用匿名用户执行的,因为我得到的状态是 200 OK.
我的测试class是这样注释的
@RunWith(SpringRunner.class)
@ActiveProfiles("dev")
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Transactional
public class TeamRestControllerTest {
//...
}
有没有人遇到过这样的问题,即嘲笑 Spring 由 Spring Social 提供的安全性?
目前我无法对其进行测试,但这里有一个可能的解决方案。
查看@WithUserDetails 实现:
@WithSecurityContext(factory = WithUserDetailsSecurityContextFactory.class)
public @interface WithUserDetails {
...
}
final class WithUserDetailsSecurityContextFactory implements
WithSecurityContextFactory<WithUserDetails> {
private BeanFactory beans;
@Autowired
public WithUserDetailsSecurityContextFactory(BeanFactory beans) {
this.beans = beans;
}
public SecurityContext createSecurityContext(WithUserDetails withUser) {
String beanName = withUser.userDetailsServiceBeanName();
UserDetailsService userDetailsService = StringUtils.hasLength(beanName)
? this.beans.getBean(beanName, UserDetailsService.class)
: this.beans.getBean(UserDetailsService.class);
String username = withUser.value();
Assert.hasLength(username, "value() must be non empty String");
UserDetails principal = userDetailsService.loadUserByUsername(username);
Authentication authentication = new UsernamePasswordAuthenticationToken(
principal, principal.getPassword(), principal.getAuthorities());
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
return context;
}
}
您可以按照相同的模式创建您选择的安全上下文:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@WithSecurityContext(factory = WithoutUserFactory.class)
public @interface WithoutUser {
}
public class WithoutUserFactory implements WithSecurityContextFactory<WithoutUser> {
public SecurityContext createSecurityContext(WithoutUser withoutUser) {
return SecurityContextHolder.createEmptyContext();
}
}
其他可用注释:WithAnonymousUser
、WithMockUser
、WithSecurityContext
(和WithUserDetails
)
添加我的解决方法,可能对其他人有帮助。
我想我遇到了同样的问题:
- A
@Testcontainers
(用于 PostgreSQL 数据库模拟)+@SpringBootTest
测试。 - 通过注释
@WithSecurityContext
和模拟factory
模拟了SecurityContext
。 - 我需要这个模拟 Envers
RevisionListener
,我从 Keycloak 通常创建的SecurityContext
中获取 userName 和 userId。 - 在测试中调用 Spring bean 时,模拟工作正常。
- 但是当通过
TestRestTemplate
调用 API 时,SecurityContext
没有被模拟,而是为所有字段(主体等)返回null
。
原来的class是这样的:
@SpringBootTest(
classes = SpringBootInitializer.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = {"keycloak.enabled=false"}
)
@ContextConfiguration(
classes = PersistenceConfiguration.class,
initializers = MyTest.Initializer.class
)
// !!! the SecurityContext mocking will NOT work when calling the controller via REST
@MockKeycloakUser() // do not fail on getting Keycloak data in UserDataRevisionListener
@EnableAutoConfiguration(exclude = { SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class }) // turn off Spring Security to avoid 401 and 302 responses
@Testcontainers // required to fill @Container fields with containers
@Log4j2
@ActiveProfiles("integration-test")
class MyTest {
@Autowired
private TestRestTemplate restTemplate;
// ...
// call via restTemplate looks like this
private List<MyDTO> executeSearchQuery(String query) {
String searchUrl = getSearchUrl(port, query, filter);
MyDTO[] results = this.restTemplate.getForObject(searchUrl, MyDTO[].class);
return List.of(results);
}
// ...
}
我用来使 SecurityContext
工作的是:
- 将
MockMvc
字段添加到测试class。 - 在测试中添加
@AutoConfigureMockMvc
class。 - !!! 通过
MockMvc
而不是TestRestTemplate
执行 API
看起来像这样:
// all other annotations on the test class stay the same
@AutoConfigureMockMvc // make MockMvc work
// ...
class MyTest {
@Autowired
private MockMvc mockMvc; // trick to make the mock SecurityContext work, which does not work when calling via TestRestTemplate
// Execute the API via mockMvc looks like this:
private String getApiResponse(MyRequest request, int expectedHttpStatus) {
final String url = getRequestUrl();
final String requestBody = JacksonUtils.serializeToString(request);
try {
final MockHttpServletRequestBuilder builder = MockMvcRequestBuilders
.post(url)
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody)
;
// use MockMvc instead of TestRestTemplate to successfully use the mock user emulation
return mockMvc
.perform(builder)
.andExpect(status().is(expectedHttpStatus))
.andReturn()
.getResponse()
.getContentAsString(StandardCharsets.UTF_8);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
// ...
}