web-dev-qa-db-ja.com

Spring3.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();
}
20
NimChimpsky

最新の春のセキュリティテストパッケージでMockMVCを使用する場合は、次のコードを試してください。

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

これを機能させるには、プリンシパルベースの認証を使用する必要があることに注意してください。

20
seanbdoherty

認証に成功すると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リクエストを行っていないことはわかっていますが、上記の統合テストとコントローラー、およびSpringSecurityの実装をより厳密に分離するのが好きです。

コードを少し冗長にするために、次のコマンドを使用して、各リクエストを行った後に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(...)には少なくとも1つの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);
_
10
Arjan

ユーザーをセキュリティコンテキストに追加するだけでよいはずです。

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

Spring 4では、SpringのセキュリティテストでCookieが返されないため、このソリューションはCookieではなくセッションを使用してformLoginとログアウトをモックします。

テストを継承することはベストプラクティスではないため、テストでこのコンポーネントを@Autowireして、そのメソッドを呼び出すことができます。

このソリューションでは、テストの最後にmockMvcを呼び出すと、performLoginで実行する各操作が認証済みとして呼び出されます。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操作を呼び出した後、テストでのすべての要求は、ログインしたユーザーとして自動的に実行されます。

2
Nagy Attila

プリンシパルを使用した解決策がうまくいかなかった理由は、別の方法で説明したいと思います。

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

さらに別の方法...私は次の注釈を使用します:

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

(ソース)

0
wrapperapps