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

Posted

技术标签:

【中文标题】如何使用 spring 3.2 新 mvc 测试登录用户【英文标题】:How to login a user with spring 3.2 new mvc testing 【发布时间】:2012-12-27 19:35:47 【问题描述】:

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

@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();

【问题讨论】:

另见Spring mvc 3.1 integration tests with session support。 【参考方案1】:

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

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

【讨论】:

是的,谢谢这修复了它,我想知道 spring 3.2 和 mockMvc 是否有新方法【参考方案2】:

如果你想使用 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()));

请记住,您必须使用基于主体的身份验证才能使其正常工作。

【讨论】:

另一种可能是使用 com.sun.security.auth.UserPrincipal: getMockMvc().perform(get("http://your-url.com").principal(new UserPrincipal("TEST_USER_ID"))).andExpect(status().isOk()));【参考方案3】:

如果成功的身份验证产生了一些 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.
 * <p>
 * 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<String, Cookie> 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);

【讨论】:

我知道这是一个旧答案,但无论如何我都会尝试:我想以完全相同的方式获取 cookie,但不知何故,我的 response().getCookies() 返回一个空数组。但我有百分之一的把握,该请求返回一个 cookie。 你在使用andReturn()吗?那我就不知道了,抱歉。 我也有同样的问题【参考方案4】:

为什么校长的解决方案对我不起作用,因此,我想提另一个出路:

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

【讨论】:

导入静态 org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;【参考方案5】:

在 spring 4 中,此解决方案使用会话而不是 cookie 模拟 formLogin 和注销,因为 spring 安全测试不返回 cookie。

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

使用此解决方案,如果您在测试结束时调用performLogin,则在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.<br/>
 * It also has more than one post processors.<br/>
 * <br/>
 * 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<SessionLogoutRequestBuilder>, Mergeable 

    private final List<RequestPostProcessor> 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<RequestPostProcessor>) 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 操作后,您在测试中的所有请求都将自动以登录用户身份执行。

【讨论】:

【参考方案6】:

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

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

(source)

【讨论】:

以上是关于如何使用 spring 3.2 新 mvc 测试登录用户的主要内容,如果未能解决你的问题,请参考以下文章

Spring MVC 3.2 Thymeleaf Ajax 片段

Spring MVC 3.2 和 HTML5 中的 Web 套接字

如何使用 Spring MVC 测试避免“圆形视图路径”异常

在 Spring Boot 1.4 MVC 测试中使用 @WebMvcTest 设置 MockMvc

如何使用 Spring MVC Test 对多部分 POST 请求进行单元测试?

在 Spring Boot 1.4 中测试 Spring MVC 切片的问题