21

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

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

6 回答 6

22

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

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

于 2013-01-14T19:34:30.753 回答
12

如果成功的身份验证产生了一些 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);
于 2013-01-24T21:55:51.417 回答
3

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

List<GrantedAuthority> list = new ArrayList<GrantedAuthority>();
list.add(new GrantedAuthorityImpl("ROLE_USER"));        
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, password,list);
SecurityContextHolder.getContext().setAuthentication(auth);
于 2013-01-13T21:28:47.217 回答
2

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

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

使用此解决方案,如果您在测试结束时调用 ,则对 的每个执行操作mockMvc都将被称为已验证。performLoginperformLogout

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

于 2017-11-02T07:15:59.017 回答
2

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

mockMvc.perform(get("your/url/{id}", 5).with(user("anyUserName")))
于 2017-02-03T10:17:56.027 回答
0

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

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

(资源)

于 2017-12-05T19:34:34.660 回答