it-swarm.com.de

Unit Testing mit Spring Security

Mein Unternehmen hat Spring MVC evaluiert, um festzustellen, ob wir es in einem unserer nächsten Projekte verwenden sollten. Bisher finde ich es toll, was ich gesehen habe, und jetzt schaue ich mir das Spring Security-Modul an, um festzustellen, ob es etwas ist, das wir verwenden können/sollten.

Unsere Sicherheitsanforderungen sind ziemlich einfach. Ein Benutzer muss lediglich einen Benutzernamen und ein Kennwort angeben können, um auf bestimmte Teile der Website zugreifen zu können (z. B. um Informationen über sein Konto abzurufen). und es gibt eine Handvoll Seiten auf der Site (FAQs, Support usw.), auf die ein anonymer Benutzer Zugriff erhalten sollte.

In dem von mir erstellten Prototyp habe ich ein "LoginCredentials" -Objekt (das nur Benutzername und Kennwort enthält) für einen authentifizierten Benutzer in Session gespeichert. Einige Controller überprüfen, ob sich dieses Objekt in einer Sitzung befindet, um beispielsweise einen Verweis auf den angemeldeten Benutzernamen zu erhalten. Ich versuche stattdessen, diese hausgemachte Logik durch Spring Security zu ersetzen, was den Vorteil hätte, dass jegliche Art von "Wie verfolgen wir angemeldete Benutzer?" und "Wie authentifizieren wir Benutzer?" von meinem Controller/Geschäftscode.

Es sieht so aus, als ob Spring Security ein (pro Thread) "Kontext" -Objekt bereitstellt, um von überall in Ihrer App auf den Benutzernamen/die Hauptinformationen zugreifen zu können ...

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

... was in gewisser Weise sehr ungewöhnlich erscheint, da dieses Objekt ein (globaler) Singleton ist.

Meine Frage lautet: Wenn dies die Standardmethode für den Zugriff auf Informationen über den authentifizierten Benutzer in Spring Security ist, wie kann ein Authentifizierungsobjekt in den SecurityContext eingefügt werden, damit es für meine Komponententests zur Verfügung steht, wenn für die Komponententests ein Kennwort erforderlich ist? authentifizierter Nutzer?

Muss ich dies in der Initialisierungsmethode jedes Testfalls verkabeln?

protected void setUp() throws Exception {
    ...
    SecurityContextHolder.getContext().setAuthentication(
        new UsernamePasswordAuthenticationToken(testUser.getLogin(), testUser.getPassword()));
    ...
}

Dies scheint zu ausführlich. Gibt es einen einfacheren Weg?

Das SecurityContextHolder -Objekt selbst scheint sehr unfrühlingartig zu sein ...

127
matt b

Das Problem ist, dass Spring Security das Authentifizierungsobjekt nicht als Bean im Container zur Verfügung stellt, sodass es nicht ohne Weiteres ohne Weiteres injiziert oder automatisch verdrahtet werden kann.

Bevor wir Spring Security verwenden, erstellen wir eine Bean mit Sitzungsbereich im Container zum Speichern des Principals, fügen diese in einen "AuthenticationService" (Singleton) ein und fügen diese Bean dann in andere Services ein, die Kenntnisse des aktuellen Principals benötigen.

Wenn Sie Ihren eigenen Authentifizierungsdienst implementieren, können Sie im Grunde das Gleiche tun: Erstellen Sie eine Bean mit Sitzungsbereich und einer "Principal" -Eigenschaft, fügen Sie diese in Ihren Authentifizierungsdienst ein, und lassen Sie die Eigenschaft vom Authentifizierungsdienst auf erfolgreiche Authentifizierung setzen Stellen Sie den Authentifizierungsservice anderen Beans nach Bedarf zur Verfügung.

Ich würde mich nicht schlecht fühlen, wenn ich SecurityContextHolder verwende. obwohl. Ich weiß, dass es sich um ein Static/Singleton handelt und Spring davon abrät, solche Dinge zu verwenden, aber bei ihrer Implementierung wird darauf geachtet, dass sie sich je nach Umgebung angemessen verhalten: sitzungsbezogen in einem Servlet-Container, threadbezogen in einem JUnit-Test usw. Der eigentliche begrenzende Faktor Ein Singleton ist, wenn es eine Implementierung bereitstellt, die für verschiedene Umgebungen unflexibel ist.

42
cliff.meyers

Tun Sie es einfach wie gewohnt und fügen Sie es dann mit SecurityContextHolder.setContext() in Ihre Testklasse ein, zum Beispiel:

Regler:

Authentication a = SecurityContextHolder.getContext().getAuthentication();

Prüfung:

Authentication authentication = Mockito.mock(Authentication.class);
// Mockito.whens() for your authorization object
SecurityContext securityContext = Mockito.mock(SecurityContext.class);
Mockito.when(securityContext.getAuthentication()).thenReturn(authentication);
SecurityContextHolder.setContext(securityContext);
139
Leonardo Eloy

Sie haben völlig Recht, sich Sorgen zu machen - statische Methodenaufrufe sind besonders problematisch für Komponententests, da Sie Ihre Abhängigkeiten nicht einfach verspotten können. Was ich Ihnen zeigen werde, ist, wie Sie den Spring-IoC-Container die Drecksarbeit für Sie erledigen lassen und Sie mit ordentlichem, testbarem Code zurücklassen. SecurityContextHolder ist eine Framework-Klasse, und obwohl es in Ordnung sein kann, dass Ihr niedriger Sicherheitscode daran gebunden ist, möchten Sie wahrscheinlich eine übersichtlichere Schnittstelle für Ihre UI-Komponenten (d. H. Controller) bereitstellen.

cliff.meyers erwähnte eine Möglichkeit, dies zu umgehen: Erstellen Sie Ihren eigenen "Principal" -Typ und fügen Sie den Konsumenten eine Instanz hinzu. Das in 2.x eingeführte Spring < aop: scoped-proxy /> -Tag in Kombination mit einer Request-Scope-Bean-Definition und die Unterstützung der Factory-Methode ist möglicherweise das Ticket für den am besten lesbaren Code.

Es könnte wie folgt funktionieren:

public class MyUserDetails implements UserDetails {
    // this is your custom UserDetails implementation to serve as a principal
    // implement the Spring methods and add your own methods as appropriate
}

public class MyUserHolder {
    public static MyUserDetails getUserDetails() {
        Authentication a = SecurityContextHolder.getContext().getAuthentication();
        if (a == null) {
            return null;
        } else {
            return (MyUserDetails) a.getPrincipal();
        }
    }
}

public class MyUserAwareController {        
    MyUserDetails currentUser;

    public void setCurrentUser(MyUserDetails currentUser) { 
        this.currentUser = currentUser;
    }

    // controller code
}

Bisher nichts kompliziertes, oder? In der Tat mussten Sie wahrscheinlich das meiste davon bereits tun. Als nächstes definieren Sie in Ihrem Bean-Kontext ein Request-Scoped-Bean, das den Principal enthält:

<bean id="userDetails" class="MyUserHolder" factory-method="getUserDetails" scope="request">
    <aop:scoped-proxy/>
</bean>

<bean id="controller" class="MyUserAwareController">
    <property name="currentUser" ref="userDetails"/>
    <!-- other props -->
</bean>

Dank der Magie des Tags aop: scoped-proxy wird die statische Methode getUserDetails jedes Mal aufgerufen, wenn eine neue HTTP-Anforderung eingeht, und alle Verweise auf die Eigenschaft currentUser werden korrekt aufgelöst. Jetzt werden Unit-Tests trivial:

protected void setUp() {
    // existing init code

    MyUserDetails user = new MyUserDetails();
    // set up user as you wish
    controller.setCurrentUser(user);
}

Hoffe das hilft!

29
Pavel

Ohne die Frage zu beantworten, wie Authentifizierungsobjekte erstellt und injiziert werden, bietet Spring Security 4.0 beim Testen einige willkommene Alternativen. Das @WithMockUser annotation ermöglicht es dem Entwickler, einen Scheinbenutzer (mit optionalen Berechtigungen, Benutzernamen, Kennwort und Rollen) auf übersichtliche Weise anzugeben:

@Test
@WithMockUser(username = "admin", authorities = { "ADMIN", "USER" })
public void getMessageWithMockUserCustomAuthorities() {
    String message = messageService.getMessage();
    ...
}

Es besteht auch die Möglichkeit, @WithUserDetails, um ein UserDetails zu emulieren, das vom UserDetailsService zurückgegeben wurde, z.

@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
    String message = messageService.getMessage();
    ...
}

Weitere Informationen finden Sie in den Kapiteln @ WithMockUser und @ WithUserDetails in den Referenzdokumenten zu Spring Security (aus denen die obigen Beispiele kopiert wurden).

24
matsev

Persönlich würde ich nur Powermock zusammen mit Mockito oder Easymock verwenden, um den statischen SecurityContextHolder.getSecurityContext () in Ihrem Unit-/Integrationstest zu verspotten, z.

@RunWith(PowerMockRunner.class)
@PrepareForTest(SecurityContextHolder.class)
public class YourTestCase {

    @Mock SecurityContext mockSecurityContext;

    @Test
    public void testMethodThatCallsStaticMethod() {
        // Set mock behaviour/expectations on the mockSecurityContext
        when(mockSecurityContext.getAuthentication()).thenReturn(...)
        ...
        // Tell mockito to use Powermock to mock the SecurityContextHolder
        PowerMockito.mockStatic(SecurityContextHolder.class);

        // use Mockito to set up your expectation on SecurityContextHolder.getSecurityContext()
        Mockito.when(SecurityContextHolder.getSecurityContext()).thenReturn(mockSecurityContext);
        ...
    }
}

Zugegebenermaßen gibt es hier eine ganze Menge Kessel-Code, z. B. ein Authentifizierungsobjekt zu verspotten, einen SecurityContext zu verspotten, um die Authentifizierung zurückzugeben, und schließlich den SecurityContextHolder zu verspotten, um den SecurityContext abzurufen. Dies ist jedoch sehr flexibel und ermöglicht es Ihnen, Tests für Szenarien wie Null-Authentifizierungsobjekte durchzuführen usw., ohne dass Sie Ihren (Nicht-Test-) Code ändern müssen

9
user404345

In diesem Fall ist die Verwendung eines statischen Codes der beste Weg, um sicheren Code zu schreiben.

Ja, Statik ist im Allgemeinen schlecht - im Allgemeinen, aber in diesem Fall ist die Statik genau das, was Sie wollen. Da der Sicherheitskontext dem aktuell ausgeführten Thread einen Principal zuordnet, würde der sicherste Code so direkt wie möglich vom Thread auf die Statik zugreifen. Durch das Verbergen des Zugriffs hinter einer Wrapper-Klasse, die injiziert wird, erhält ein Angreifer mehr Angriffspunkte. Sie würden keinen Zugriff auf den Code benötigen (was sich beim Signieren der JAR-Datei nur schwer tun würde). Sie benötigen lediglich eine Möglichkeit, die Konfiguration zu überschreiben. Dies kann zur Laufzeit erfolgen oder indem Sie XML in den Klassenpfad einfügen. Selbst die Verwendung von Annotation Injection wäre mit externem XML überschreibbar. Solches XML könnte das laufende System mit einem Rogue-Prinzipal versorgen.

5
Michael Bushe

Ich habe die gleiche Frage selbst über hier gestellt und gerade eine Antwort gepostet, die ich kürzlich gefunden habe. Die kurze Antwort lautet: Fügen Sie ein SecurityContext ein und beziehen Sie sich nur in Ihrer Spring-Konfiguration auf SecurityContextHolder, um das SecurityContext zu erhalten.

4
Scott Bale

Allgemeines

In der Zwischenzeit (seit Version 3.2, im Jahr 2013, dank SEC-2298 ) kann die Authentifizierung mithilfe der Annotation @ AuthenticationPrincipal in MVC-Methoden eingefügt werden:

@Controller
class Controller {
  @RequestMapping("/somewhere")
  public void doStuff(@AuthenticationPrincipal UserDetails myUser) {
  }
}

Tests

In Ihrem Unit-Test können Sie diese Methode natürlich direkt aufrufen. In Integrationstests mit org.springframework.test.web.servlet.MockMvc Können Sie org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user() verwenden, um den Benutzer folgendermaßen einzuschleusen:

mockMvc.perform(get("/somewhere").with(user(myUserDetails)));

Dies wird jedoch nur direkt den SecurityContext füllen. Wenn Sie sicherstellen möchten, dass der Benutzer aus einer Sitzung in Ihrem Test geladen wird, können Sie dies verwenden:

mockMvc.perform(get("/somewhere").with(sessionUser(myUserDetails)));
/* ... */
private static RequestPostProcessor sessionUser(final UserDetails userDetails) {
    return new RequestPostProcessor() {
        @Override
        public MockHttpServletRequest postProcessRequest(final MockHttpServletRequest request) {
            final SecurityContext securityContext = new SecurityContextImpl();
            securityContext.setAuthentication(
                new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities())
            );
            request.getSession().setAttribute(
                HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, securityContext
            );
            return request;
        }
    };
}
2
yankee

Ich würde einen Blick auf die abstrakten Testklassen von Spring werfen und Objekte verspotten, über die gesprochen wird hier . Sie bieten eine leistungsstarke Möglichkeit zur automatischen Verkabelung Ihrer Spring-verwalteten Objekte, wodurch das Testen von Einheiten und Integrationen vereinfacht wird.

2
digitalsanctum

Die Authentifizierung ist eine Eigenschaft eines Threads in einer Serverumgebung auf dieselbe Weise wie eine Eigenschaft eines Prozesses in einem Betriebssystem. Eine Bean-Instanz für den Zugriff auf Authentifizierungsinformationen zu haben, wäre unpraktisch für die Konfiguration und den Verkabelungsaufwand, ohne dass dies von Vorteil wäre.

In Bezug auf die Testauthentifizierung gibt es verschiedene Möglichkeiten, wie Sie Ihr Leben einfacher gestalten können. Mein Favorit ist es, eine benutzerdefinierte Anmerkung zu machen @Authenticated und Testausführungs-Listener, der es verwaltet. Überprüfen Sie DirtiesContextTestExecutionListener auf Inspiration.

1
Pavel Horal

Nach ziemlich viel Arbeit konnte ich das gewünschte Verhalten reproduzieren. Ich hatte das Login über MockMvc emuliert. Es ist zu schwer für die meisten Unit-Tests, aber hilfreich für Integrationstests.

Natürlich bin ich bereit, die neuen Funktionen in Spring Security 4.0 zu sehen, die das Testen erleichtern.

package [myPackage]

import static org.junit.Assert.*;

import javax.inject.Inject;
import javax.servlet.http.HttpSession;

import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
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;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;

@ContextConfiguration(locations={[my config file locations]})
@WebAppConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public static class getUserConfigurationTester{

    private MockMvc mockMvc;

    @Autowired
    private FilterChainProxy springSecurityFilterChain;

    @Autowired
    private MockHttpServletRequest request;

    @Autowired
    private WebApplicationContext webappContext;

    @Before  
    public void init() {  
        mockMvc = MockMvcBuilders.webAppContextSetup(webappContext)
                    .addFilters(springSecurityFilterChain)
                    .build();
    }  


    @Test
    public void testTwoReads() throws Exception{                        

    HttpSession session  = mockMvc.perform(post("/j_spring_security_check")
                        .param("j_username", "admin_001")
                        .param("j_password", "secret007"))
                        .andDo(print())
                        .andExpect(status().isMovedTemporarily())
                        .andExpect(redirectedUrl("/index"))
                        .andReturn()
                        .getRequest()
                        .getSession();

    request.setSession(session);

    SecurityContext securityContext = (SecurityContext)   session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);

    SecurityContextHolder.setContext(securityContext);

        // Your test goes here. User is logged with 
}
0
borjab