如何使用spring 3.2新的mvc测试登录用户

这工作正常,直到我必须测试需要登录用户的服务,如何将用户添加到上下文:

@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:applicationContext-test.xml") @WebAppConfiguration public class FooTest { @Autowired private WebApplicationContext webApplicationContext; private MockMvc mockMvc; @Resource(name = "aService") private AService aService; //uses logged in user @Before public void setup() { this.mockMvc = webAppContextSetup(this.webApplicationContext).build(); } 

如果您想将MockMVC与最新的spring安全测试包一起使用,请尝试以下代码:

 Principal principal = new Principal() { @Override public String getName() { return "TEST_PRINCIPAL"; } }; getMockMvc().perform(get("http://your-url.com").principal(principal)) .andExpect(status().isOk())); 

请记住,您必须使用基于Principal的身份validation才能使用此function。

如果成功的身份validation产生了一些cookie,那么您可以捕获它(或只是所有cookie),并在下一个测试中传递它:

 @Autowired private WebApplicationContext wac; @Autowired private FilterChainProxy filterChain; private MockMvc mockMvc; @Before public void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac) .addFilter(filterChain).build(); } @Test public void testSession() throws Exception { // Login and save the cookie MvcResult result = mockMvc.perform(post("/session") .param("username", "john").param("password", "s3cr3t")).andReturn(); Cookie c = result.getResponse().getCookie("my-cookie"); assertThat(c.getValue().length(), greaterThan(10)); // No cookie; 401 Unauthorized mockMvc.perform(get("/personal").andExpect(status().isUnauthorized()); // With cookie; 200 OK mockMvc.perform(get("/personal").cookie(c)).andExpect(status().isOk()); // Logout, and ensure we're told to wipe the cookie result = mockMvc.perform(delete("/session").andReturn(); c = result.getResponse().getCookie("my-cookie"); assertThat(c.getValue().length(), is(0)); } 

虽然我知道我在这里没有提出任何HTTP请求,但我有点像上面的集成测试和我的控制器以及Spring Security实现的更严格的分离。

为了使代码更简洁,我在发出每个请求后使用以下内容合并cookie,然后在每个后续请求中传递这些cookie:

 /** * Merges the (optional) existing array of Cookies with the response in the * given MockMvc ResultActions. * 

* This only adds or deletes cookies. Officially, we should expire old * cookies. But we don't keep track of when they were created, and this is * not currently required in our tests. */ protected static Cookie[] updateCookies(final Cookie[] current, final ResultActions result) { final Map currentCookies = new HashMap<>(); if (current != null) { for (Cookie c : current) { currentCookies.put(c.getName(), c); } } final Cookie[] newCookies = result.andReturn().getResponse().getCookies(); for (Cookie newCookie : newCookies) { if (StringUtils.isBlank(newCookie.getValue())) { // An empty value implies we're told to delete the cookie currentCookies.remove(newCookie.getName()); } else { // Add, or replace: currentCookies.put(newCookie.getName(), newCookie); } } return currentCookies.values().toArray(new Cookie[currentCookies.size()]); }

……作为cookie(...)的小帮手需要至少一个cookie:

 /** * Creates an array with a dummy cookie, useful as Spring MockMvc * {@code cookie(...)} does not like {@code null} values or empty arrays. */ protected static Cookie[] initCookies() { return new Cookie[] { new Cookie("unittest-dummy", "dummy") }; } 

……最终得到:

 Cookie[] cookies = initCookies(); ResultActions actions = mockMvc.perform(get("/personal").cookie(cookies) .andExpect(status().isUnauthorized()); cookies = updateCookies(cookies, actions); actions = mockMvc.perform(post("/session").cookie(cookies) .param("username", "john").param("password", "s3cr3t")); cookies = updateCookies(cookies, actions); actions = mockMvc.perform(get("/personal").cookie(cookies)) .andExpect(status().isOk()); cookies = updateCookies(cookies, actions); 

您应该只需将用户添加到安全上下文中:

 List list = new ArrayList(); list.add(new GrantedAuthorityImpl("ROLE_USER")); UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, password,list); SecurityContextHolder.getContext().setAuthentication(auth); 

使用spring 4,此解决方案使用会话而非cookie来模拟formLogin和logout,因为spring安全测试不返回cookie。

因为inheritance测试不是最佳实践,所以可以在测试中@Autowire这个组件并调用它的方法。

使用此解决方案,如果在测试结束时调用performLogout ,则可以调用performLogout ,对mockMvc执行每个操作都可以调用performLogout

 import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpSession; import org.springframework.stereotype.Component; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import javax.servlet.Filter; import static com.condix.SessionLogoutRequestBuilder.sessionLogout; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated; import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @Component public class SessionBasedMockMvc { private static final String HOME_PATH = "/"; private static final String LOGOUT_PATH = "/login?logout"; @Autowired private WebApplicationContext webApplicationContext; @Autowired private Filter springSecurityFilterChain; private MockMvc mockMvc; public MockMvc createSessionBasedMockMvc() { final MockHttpServletRequestBuilder defaultRequestBuilder = get("/dummy-path"); this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext) .defaultRequest(defaultRequestBuilder) .alwaysDo(result -> setSessionBackOnRequestBuilder(defaultRequestBuilder, result.getRequest())) .apply(springSecurity(springSecurityFilterChain)) .build(); return this.mockMvc; } public void performLogin(final String username, final String password) throws Exception { final ResultActions resultActions = this.mockMvc.perform(formLogin().user(username).password(password)); this.assertSuccessLogin(resultActions); } public void performLogout() throws Exception { final ResultActions resultActions = this.mockMvc.perform(sessionLogout()); this.assertSuccessLogout(resultActions); } private MockHttpServletRequest setSessionBackOnRequestBuilder(final MockHttpServletRequestBuilder requestBuilder, final MockHttpServletRequest request) { requestBuilder.session((MockHttpSession) request.getSession()); return request; } private void assertSuccessLogin(final ResultActions resultActions) throws Exception { resultActions.andExpect(status().isFound()) .andExpect(authenticated()) .andExpect(redirectedUrl(HOME_PATH)); } private void assertSuccessLogout(final ResultActions resultActions) throws Exception { resultActions.andExpect(status().isFound()) .andExpect(unauthenticated()) .andExpect(redirectedUrl(LOGOUT_PATH)); } } 

由于默认LogoutRequestBuilder不支持会话,因此我们需要创建另一个注销请求构建器。

 import org.springframework.beans.Mergeable; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpSession; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.request.ConfigurableSmartRequestBuilder; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import javax.servlet.ServletContext; import java.util.ArrayList; import java.util.List; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; /** * This is a logout request builder which allows to send the session on the request.
* It also has more than one post processors.
*
* Unfortunately it won't trigger {@link org.springframework.security.core.session.SessionDestroyedEvent} because * that is triggered by {@link org.apache.catalina.session.StandardSessionFacade#invalidate()} in Tomcat and * for mocks it's handled by @{{@link MockHttpSession#invalidate()}} so the log out message won't be visible for tests. */ public final class SessionLogoutRequestBuilder implements ConfigurableSmartRequestBuilder, Mergeable { private final List postProcessors = new ArrayList<>(); private String logoutUrl = "/logout"; private MockHttpSession session; private SessionLogoutRequestBuilder() { this.postProcessors.add(csrf()); } static SessionLogoutRequestBuilder sessionLogout() { return new SessionLogoutRequestBuilder(); } @Override public MockHttpServletRequest buildRequest(final ServletContext servletContext) { return post(this.logoutUrl).session(session).buildRequest(servletContext); } public SessionLogoutRequestBuilder logoutUrl(final String logoutUrl) { this.logoutUrl = logoutUrl; return this; } public SessionLogoutRequestBuilder session(final MockHttpSession session) { Assert.notNull(session, "'session' must not be null"); this.session = session; return this; } @Override public boolean isMergeEnabled() { return true; } @SuppressWarnings("unchecked") @Override public Object merge(final Object parent) { if (parent == null) { return this; } if (parent instanceof MockHttpServletRequestBuilder) { final MockHttpServletRequestBuilder parentBuilder = (MockHttpServletRequestBuilder) parent; if (this.session == null) { this.session = (MockHttpSession) ReflectionTestUtils.getField(parentBuilder, "session"); } final List postProcessors = (List) ReflectionTestUtils.getField(parentBuilder, "postProcessors"); this.postProcessors.addAll(0, (List) postProcessors); } else if (parent instanceof SessionLogoutRequestBuilder) { final SessionLogoutRequestBuilder parentBuilder = (SessionLogoutRequestBuilder) parent; if (!StringUtils.hasText(this.logoutUrl)) { this.logoutUrl = parentBuilder.logoutUrl; } if (this.session == null) { this.session = parentBuilder.session; } this.postProcessors.addAll(0, parentBuilder.postProcessors); } else { throw new IllegalArgumentException("Cannot merge with [" + parent.getClass().getName() + "]"); } return this; } @Override public SessionLogoutRequestBuilder with(final RequestPostProcessor postProcessor) { Assert.notNull(postProcessor, "postProcessor is required"); this.postProcessors.add(postProcessor); return this; } @Override public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { for (final RequestPostProcessor postProcessor : this.postProcessors) { request = postProcessor.postProcessRequest(request); if (request == null) { throw new IllegalStateException( "Post-processor [" + postProcessor.getClass().getName() + "] returned null"); } } return request; } }

调用performLogin操作后,测试中的所有请求将按登录用户自动执行。

一些为什么与校长的解决方案对我没有用,因此,我想提一下另一种方法:

 mockMvc.perform(get("your/url/{id}", 5).with(user("anyUserName"))) 

另一种方式……我使用以下注释:

 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration @TestExecutionListeners(listeners={ServletTestExecutionListener.class, DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class, WithSecurityContextTestExcecutionListener.class}) @WithMockUser public class WithMockUserTests { ... } 

(资源)