Spring Test& Security: jak sfałszować uwierzytelnianie?

Próbowałem dowiedzieć się, jak przetestować jednostki, czy Moje adresy URL moich kontrolerów są prawidłowo zabezpieczone. Na wypadek, gdyby ktoś coś zmienił i przypadkowo usunął ustawienia zabezpieczeń.

Moja metoda kontrolera wygląda tak:

@RequestMapping("/api/v1/resource/test") 
@Secured("ROLE_USER")
public @ResonseBody String test() {
    return "test";
}

Założyłem taki WebTestEnvironment:

import javax.annotation.Resource;
import javax.naming.NamingException;
import javax.sql.DataSource;

import org.junit.Before;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({ 
        "file:src/main/webapp/WEB-INF/spring/security.xml",
        "file:src/main/webapp/WEB-INF/spring/applicationContext.xml",
        "file:src/main/webapp/WEB-INF/spring/servlet-context.xml" })
public class WebappTestEnvironment2 {

    @Resource
    private FilterChainProxy springSecurityFilterChain;

    @Autowired
    @Qualifier("databaseUserService")
    protected UserDetailsService userDetailsService;

    @Autowired
    private WebApplicationContext wac;

    @Autowired
    protected DataSource dataSource;

    protected MockMvc mockMvc;

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    protected UsernamePasswordAuthenticationToken getPrincipal(String username) {

        UserDetails user = this.userDetailsService.loadUserByUsername(username);

        UsernamePasswordAuthenticationToken authentication = 
                new UsernamePasswordAuthenticationToken(
                        user, 
                        user.getPassword(), 
                        user.getAuthorities());

        return authentication;
    }

    @Before
    public void setupMockMvc() throws NamingException {

        // setup mock MVC
        this.mockMvc = MockMvcBuilders
                .webAppContextSetup(this.wac)
                .addFilters(this.springSecurityFilterChain)
                .build();
    }
}

W moim prawdziwym teście próbowałem zrobić coś takiego:

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Test;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;

import eu.ubicon.webapp.test.WebappTestEnvironment;

public class CopyOfClaimTest extends WebappTestEnvironment {

    @Test
    public void signedIn() throws Exception {

        UsernamePasswordAuthenticationToken principal = 
                this.getPrincipal("test1");

        SecurityContextHolder.getContext().setAuthentication(principal);        

        super.mockMvc
            .perform(
                    get("/api/v1/resource/test")
//                    .principal(principal)
                    .session(session))
            .andExpect(status().isOk());
    }

}

Podniosłem to tutaj:

Jednak jeśli się przyjrzeć, to pomaga tylko wtedy, gdy nie wysyła rzeczywistych wniosków do Adresów URL, ale tylko podczas testowania usług na poziomie funkcji. W moim przypadku został wyrzucony wyjątek "access denied":

org.springframework.security.access.AccessDeniedException: Access is denied
    at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:83) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:206) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
    at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:60) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) ~[spring-aop-3.2.1.RELEASE.jar:3.2.1.RELEASE]
        ...

Na uwagę zasługują następujące dwa komunikaty dziennika, które mówią, że żaden użytkownik nie został uwierzytelniony, wskazując, że ustawienie Principal nie zadziałało lub zostało nadpisane.

14:20:34.454 [main] DEBUG o.s.s.a.i.a.MethodSecurityInterceptor - Secure object: ReflectiveMethodInvocation: public java.util.List test.TestController.test(); target is of class [test.TestController]; Attributes: [ROLE_USER]
14:20:34.454 [main] DEBUG o.s.s.a.i.a.MethodSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken@9055e4a6: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS
Author: Community, 2013-03-04

7 answers

Okazało się, że SecurityContextPersistenceFilter, który jest częścią łańcucha filtrów zabezpieczających sprężynę, zawsze resetuje moje SecurityContext, które ustawiłem wywołaniem SecurityContextHolder.getContext().setAuthentication(principal) (lub przy użyciu metody .principal(principal)). Ten filtr Ustawia SecurityContext w SecurityContextHolder z SecurityContext z SecurityContextRepository nadpisanie tego, który ustawiłem wcześniej. Repozytorium to domyślnie HttpSessionSecurityContextRepository. HttpSessionSecurityContextRepository sprawdza podany HttpRequest i próbuje uzyskać dostęp do odpowiedniego HttpSession. Jeśli istnieje, spróbuje odczytać {[2] } z HttpSession. Jeśli to się nie powiedzie, repozytorium generuje puste SecurityContext.

Tak więc moim rozwiązaniem jest przekazanie HttpSession wraz z żądaniem, które zawiera SecurityContext:
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Test;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;

import eu.ubicon.webapp.test.WebappTestEnvironment;

public class Test extends WebappTestEnvironment {

    public static class MockSecurityContext implements SecurityContext {

        private static final long serialVersionUID = -1386535243513362694L;

        private Authentication authentication;

        public MockSecurityContext(Authentication authentication) {
            this.authentication = authentication;
        }

        @Override
        public Authentication getAuthentication() {
            return this.authentication;
        }

        @Override
        public void setAuthentication(Authentication authentication) {
            this.authentication = authentication;
        }
    }

    @Test
    public void signedIn() throws Exception {

        UsernamePasswordAuthenticationToken principal = 
                this.getPrincipal("test1");

        MockHttpSession session = new MockHttpSession();
        session.setAttribute(
                HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, 
                new MockSecurityContext(principal));


        super.mockMvc
            .perform(
                    get("/api/v1/resource/test")
                    .session(session))
            .andExpect(status().isOk());
    }
}
 44
Author: Martin Becker,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-07-09 22:58:05

Seaching dla odpowiedzi nie mogłem znaleźć żadnego, aby być łatwym i elastycznym w tym samym czasie, potem znalazłem Spring Security Reference i zdałem sobie sprawę, że są blisko idealnych rozwiązań. Rozwiązania AOP często są najlepsze do testowania, A Spring zapewnia mu @WithMockUser, @WithUserDetails i @WithSecurityContext, w tym artefakcie:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <version>4.2.2.RELEASE</version>
    <scope>test</scope>
</dependency>

W większości przypadków, @WithUserDetails gromadzi elastyczność i moc, której potrzebuję.

Jak działa @ WithUserDetails?

W zasadzie wystarczy stworzyć custom UserDetailsService ze wszystkimi możliwymi profilami użytkowników, które chcesz przetestować. E. g

@TestConfiguration
public class SpringSecurityWebAuxTestConfig {

    @Bean
    @Primary
    public UserDetailsService userDetailsService() {
        User basicUser = new UserImpl("Basic User", "[email protected]", "password");
        UserActive basicActiveUser = new UserActive(basicUser, Arrays.asList(
                new SimpleGrantedAuthority("ROLE_USER"),
                new SimpleGrantedAuthority("PERM_FOO_READ")
        ));

        User managerUser = new UserImpl("Manager User", "[email protected]", "password");
        UserActive managerActiveUser = new UserActive(managerUser, Arrays.asList(
                new SimpleGrantedAuthority("ROLE_MANAGER"),
                new SimpleGrantedAuthority("PERM_FOO_READ"),
                new SimpleGrantedAuthority("PERM_FOO_WRITE"),
                new SimpleGrantedAuthority("PERM_FOO_MANAGE")
        ));

        return new InMemoryUserDetailsManager(Arrays.asList(
                basicActiveUser, managerActiveUser
        ));
    }
}

Teraz mamy naszych użytkowników gotowych, więc wyobraź sobie, że chcemy przetestować kontrolę dostępu do tej funkcji kontrolera: {]}

@RestController
@RequestMapping("/foo")
public class FooController {

    @Secured("ROLE_MANAGER")
    @GetMapping("/salute")
    public String saluteYourManager(@AuthenticationPrincipal User activeUser)
    {
        return String.format("Hi %s. Foo salutes you!", activeUser.getUsername());
    }
}

Tutaj mamy get mapped function do trasy /foo/salute i testujemy zabezpieczenia oparte na rolach z adnotacją @Secured, chociaż możesz również przetestować @PreAuthorize i @PostAuthorize. Stwórzmy dwa testy, jeden, aby sprawdzić, czy prawidłowy użytkownik może to zobaczyć odpowiedź i drugi, aby sprawdzić, czy jest to rzeczywiście zabronione.

@RunWith(SpringRunner.class)
@SpringBootTest(
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        classes = SpringSecurityWebAuxTestConfig.class
)
@AutoConfigureMockMvc
public class WebApplicationSecurityTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    @WithUserDetails("[email protected]")
    public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception
    {
        mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
                .accept(MediaType.ALL))
                .andExpect(status().isOk())
                .andExpect(content().string(containsString("[email protected]")));
    }

    @Test
    @WithUserDetails("[email protected]")
    public void givenBasicUser_whenGetFooSalute_thenForbidden() throws Exception
    {
        mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
                .accept(MediaType.ALL))
                .andExpect(status().isForbidden());
    }
}

Jak widzisz zaimportowaliśmy SpringSecurityWebAuxTestConfig, aby zapewnić naszym użytkownikom testy. Każdy z nich został użyty w odpowiednim przypadku testowym poprzez użycie prostej adnotacji, zmniejszając kod i złożoność.

Lepiej używać @ WithMockUser dla prostszego bezpieczeństwa opartego na rolach

Jak widzisz @WithUserDetails ma całą elastyczność, jakiej potrzebujesz dla większości aplikacji. Pozwala na korzystanie z niestandardowych użytkowników z dowolnym GrantedAuthority, takich jak role lub uprawnienia. Ale jeśli pracujesz tylko z rolami, testowanie może być jeszcze łatwiejsze i możesz uniknąć konstruowania niestandardowego UserDetailsService. W takich przypadkach określ prostą kombinację użytkownika, hasła i ról za pomocą @WithMockUser.

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@WithSecurityContext(
    factory = WithMockUserSecurityContextFactory.class
)
public @interface WithMockUser {
    String value() default "user";

    String username() default "";

    String[] roles() default {"USER"};

    String password() default "password";
}

Adnotacja definiuje domyślne wartości dla bardzo podstawowego użytkownika. Ponieważ w naszym przypadku testowana trasa wymaga tylko, aby uwierzytelniony użytkownik był menedżerem, możemy zakończyć korzystanie z SpringSecurityWebAuxTestConfig i zrobić to.

@Test
@WithMockUser(roles = "MANAGER")
public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception
{
    mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
            .accept(MediaType.ALL))
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("user")));
}

Zauważ, że teraz zamiast użytkownika [email protected] otrzymujemy domyślną wartość dostarczoną przez @WithMockUser: user ; ale to nie ma znaczenia, bo tak naprawdę zależy nam na jego roli: ROLE_MANAGER.

Wnioski

Jak widzisz z adnotacjami, takimi jak @WithUserDetails i @WithMockUser, możemy przełączać się między różnymi scenariuszami uwierzytelnionych użytkowników bez budowania klas wyobcowanych z naszej architektury tylko dla wykonywania prostych testów. Zaleca się również, aby zobaczyć, jak @ WithSecurityContext Działa na jeszcze większą elastyczność.

 32
Author: EliuX,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-01 17:10:21

Dodaj pom.xml:

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <version>4.0.0.RC2</version>
    </dependency>

I użyj org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors do żądania autoryzacji. Zobacz przykładowe użycie w https://github.com/rwinch/spring-security-test-blog ( https://jira.spring.io/browse/SEC-2592).

Aktualizacja:

4.0.0.RC2 działa na spring-security 3.x. Dla spring-security 4 Spring-security-test zostań częścią spring-security ( http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test , wersja jest taka sama).

Konfiguracja została zmieniona: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-mockmvc

public void setup() {
    mvc = MockMvcBuilders
            .webAppContextSetup(context)
            .apply(springSecurity())  
            .build();
}

Próbka dla uwierzytelniania podstawowego: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#testing-http-basic-authentication.

 31
Author: GKislin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-26 14:11:40

Od wiosny 4.0+ najlepszym rozwiązaniem jest adnotacja metody testowej za pomocą @ WithMockUser

@Test
@WithMockUser(username = "user1", password = "pwd", roles = "USER")
public void mytest1() throws Exception {
    mockMvc.perform(get("/someApi"))
        .andExpect(status().isOk());
}

Pamiętaj, aby dodać następującą zależność do projektu

'org.springframework.security:spring-security-test:4.2.3.RELEASE'
 13
Author: GummyBear21,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-30 13:51:27

Oto przykład dla tych, którzy chcą przetestować konfigurację zabezpieczeń Spring MockMvc przy użyciu uwierzytelniania Base64 basic.

String basicDigestHeaderValue = "Basic " + new String(Base64.encodeBase64(("<username>:<password>").getBytes()));
this.mockMvc.perform(get("</get/url>").header("Authorization", basicDigestHeaderValue).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());

Zależność Mavena

    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
        <version>1.3</version>
    </dependency>
 6
Author: Jay,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-02-25 18:33:14

Krótka odpowiedź:

@Autowired
private WebApplicationContext webApplicationContext;

@Autowired
private Filter springSecurityFilterChain;

@Before
public void setUp() throws Exception {
    final MockHttpServletRequestBuilder defaultRequestBuilder = get("/dummy-path");
    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext)
            .defaultRequest(defaultRequestBuilder)
            .alwaysDo(result -> setSessionBackOnRequestBuilder(defaultRequestBuilder, result.getRequest()))
            .apply(springSecurity(springSecurityFilterChain))
            .build();
}

private MockHttpServletRequest setSessionBackOnRequestBuilder(final MockHttpServletRequestBuilder requestBuilder,
                                                             final MockHttpServletRequest request) {
    requestBuilder.session((MockHttpSession) request.getSession());
    return request;
}

Po wykonaniu formLogin Z spring security test każde z Twoich zapytań zostanie automatycznie wywołane jako zalogowany użytkownik.

Długa odpowiedź:

Sprawdź To rozwiązanie (odpowiedź jest dla spring 4): Jak zalogować się do użytkownika z spring 3.2 new mvc testing

 3
Author: Nagy Attila,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-02 07:49:31

Opcje unikania SecurityContextHolder w testach:

  • Option 1 : use mocks-mam na myśli mock SecurityContextHolder using some mock library - EasyMock for example
  • Opcja 2 : zawijanie wywołania SecurityContextHolder.get... w kodzie w jakiejś usłudze - na przykład w {[2] } z metodą getCurrentPrincipal implementującą interfejs SecurityService, a następnie w testach możesz po prostu stworzyć przykładową implementację tego interfejsu, która zwróci żądaną wartość główną bez dostępu do SecurityContextHolder.
 2
Author: Pavla Nováková,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-04 17:38:25