2008-12-11 23 views
94

Meine Firma hat Spring MVC evaluiert, um festzustellen, ob wir es in einem unserer nächsten Projekte verwenden sollten. Bisher liebe ich, was ich gesehen habe, und jetzt schaue ich mir das Spring Security-Modul an, um festzustellen, ob wir etwas verwenden können/sollten.Komponententests mit Spring Security

Unsere Sicherheitsanforderungen sind ziemlich einfach; Ein Benutzer muss lediglich einen Benutzernamen und ein Passwort angeben können, um auf bestimmte Teile der Website zugreifen zu können (z. B. um Informationen über sein Konto zu erhalten). und es gibt eine Handvoll Seiten auf der Seite (FAQs, Support, usw.), wo ein anonymer Benutzer Zugang erhalten soll.

In dem Prototyp, den ich erstellt habe, habe ich ein "LoginCredentials" -Objekt (das nur Benutzername und Passwort enthält) in der Sitzung für einen authentifizierten Benutzer gespeichert; Einige der Controller überprüfen, ob dieses Objekt in der Sitzung ist, um beispielsweise einen Verweis auf den angemeldeten Benutzernamen zu erhalten. Ich möchte stattdessen diese hauseigene Logik durch Spring Security ersetzen, was den Vorteil hätte, jede Art von "Wie verfolgen wir eingeloggte Benutzer?" Zu entfernen. und "Wie authentifizieren wir Benutzer?" von meinem Controller/Geschäftscode.

Es scheint, wie Spring Security bietet eine (pro Thread) „Kontext“ Objekt der Lage sein, den Benutzernamen/Hauptinfo in Ihrer Anwendung von überall zugreifen ...

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

... das scheint sehr un-Frühling wie dieses Objekt ein (globaler) Singleton ist, in gewisser Weise.

Meine Frage ist dies: Wenn dies die Standardmethode ist, um auf Informationen über den authentifizierten Benutzer in Spring Security zuzugreifen, was ist die akzeptierte Methode, ein Authentication Objekt in den SecurityContext zu injizieren, damit es für meine Unit Tests verfügbar ist Komponententests erfordern einen authentifizierten Benutzer?

Muss ich dies bei der Initialisierung jedes Testfalls verdrahten?

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

Dies scheint übermäßig ausführlich. Gibt es einen leichteren Weg?

Das SecurityContextHolder Objekt selbst scheint sehr un-Spring-like ...

Antwort

31

Das Problem besteht darin, dass Spring Security das Authentifizierungsobjekt nicht als Bean im Container zur Verfügung stellt, sodass es nicht möglich ist, es einfach aus der Box zu injizieren oder zu autowire.

Bevor wir mit Spring Security begannen, erstellten wir im Container eine Session-Scoped-Bean, um den Principal zu speichern, ihn in einen "AuthenticationService" (Singleton) zu injizieren und dann diese Bean in andere Dienste zu injizieren der aktuelle Auftraggeber.

Wenn Sie Ihren eigenen Authentifizierungsdienst implementieren, können Sie grundsätzlich dasselbe tun: Erstellen Sie ein sessionscoped Bean mit einer "Principal" -Eigenschaft, injizieren Sie dies in Ihren Authentifizierungsdienst, lassen Sie den Autorisierungsdienst die Eigenschaft erfolgreich setzen auth, und dann den Auth-Dienst für andere Beans verfügbar machen, wie Sie es brauchen.

Ich wäre nicht schlecht über die Verwendung von SecurityContextHolder. obwohl.Ich weiß, dass es ein Static/Singleton ist und dass Spring davon abrät, solche Dinge zu verwenden, aber ihre Implementierung sorgt dafür, dass sie sich in Abhängigkeit von der Umgebung angemessen verhalten: Session-Bereich in einem Servlet-Container, Thread-Bereich in einem JUnit-Test, etc. Der eigentliche limitierende Faktor von einem Singleton ist, wenn es eine Implementierung bietet, die für verschiedene Umgebungen unflexibel ist.

+0

Danke, das ist ein nützlicher Rat. Was ich bisher gemacht habe, ist im Wesentlichen, mit dem Aufruf von SecurityContextHolder.getContext() fortzufahren (durch ein paar eigene Wrapper-Methoden, so dass es zumindest von einer Klasse aufgerufen wird). –

+2

Obwohl nur eine Anmerkung - ich glaube nicht, ServletContextHolder hat ein Konzept von HttpSession oder eine Möglichkeit zu wissen, ob es in einer Web-Server-Umgebung funktioniert - es verwendet ThreadLocal, wenn Sie es für etwas anderes konfigurieren (die einzigen anderen zwei eingebauten Modi sind) InheritableThreadLocal und Global) –

+0

Der einzige Nachteil bei der Verwendung von session/request-scoped Beans im Frühling ist, dass sie in einem JUnit-Test fehlschlagen. Was Sie tun können, ist ein benutzerdefinierter Bereich zu implementieren, der Sitzung/Anfrage verwendet, falls verfügbar, und auf den Thread zurückfallen muss. Meine Vermutung ist, dass Spring Security etwas Ähnliches macht ... –

2

ich einen Blick auf Spring abstrakter Testklassen und Mock-Objekte nehmen würde, die here darüber gesprochen werden. Sie bieten eine leistungsstarke Möglichkeit zur automatischen Verdrahtung von Spring-verwalteten Objekten, wodurch die Einheits- und Integrationstests vereinfacht werden.

+0

Während diese Testklassen sind hilfreich finden, ich bin nicht sicher, ob Sie gelten hier.Meine Tests haben kein Konzept des ApplicationContext - sie brauchen keinen. Alles, was ich brauche, ist sicherzustellen, dass der SecurityContext aufgefüllt ist, bevor die Testmethode ausgeführt wird - es fühlt sich einfach dreckig an, es zuerst in einem ThreadLocal setzen zu müssen. –

26

Sie haben recht zu befürchten - statische Methodenaufrufe sind besonders problematisch für Komponententests, da Sie Ihre Abhängigkeiten nicht leicht verspotten können. Was ich Ihnen zeigen werde, ist, wie Sie mit dem Spring IoC Container die schmutzige Arbeit für Sie erledigen können und Sie mit ordentlichem, testbarem Code belassen. SecurityContextHolder ist eine Framework-Klasse und obwohl es in Ordnung sein könnte, dass Ihr Low-Level-Sicherheitscode daran gebunden ist, möchten Sie wahrscheinlich eine bessere Schnittstelle zu Ihren UI-Komponenten (z. B. Controllern) bereitstellen.

cliff.meyers erwähnt einen Weg um es herum - erstellen Sie Ihren eigenen "Principal" -Typ und injizieren Sie eine Instanz in die Verbraucher. Das in 2.x eingeführte Spring-Tag < aop:scoped-proxy /> in Verbindung mit einer Anforderungsbereichs-Bean-Definition und die Unterstützung der Factory-Methode können das Ticket für den am besten lesbaren Code sein.

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 
} 

Nichts bisher kompliziert, nicht wahr? Wahrscheinlich mussten Sie das meiste schon tun. Als nächstes wird in Ihrem Bean Kontext einer Anfrage-scoped Bohne definieren den Haupt zu halten:

<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 AOP: scoped-Proxy-Tag, die statische Methode getUserDetails genannt eine neue HTTP-Anforderung jedes Mal wird kommt herein und alle Verweise auf die currentUser-Eigenschaft werden korrekt aufgelöst. Jetzt Unit-Tests wird trivial:

protected void setUp() { 
    // existing init code 

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

hoffe, das hilft!

+3

Dies ist der beste Vorschlag, den ich gesehen habe, um dieses Problem anzugehen, ich kann nicht glauben, dass Spring nicht so etwas gebaut hat. –

3

Ich fragte die gleiche Frage selbst über here, und schrieb gerade eine Antwort, die ich vor kurzem gefunden. Kurze Antwort ist: injizieren Sie eine SecurityContext, und beziehen Sie sich auf SecurityContextHolder nur in Ihrer Spring-Konfiguration, um die SecurityContext

5

zu erhalten Verwenden einer statischen in diesem Fall ist der beste Weg, um sicheren Code zu schreiben.

Ja, Statik ist im Allgemeinen schlecht - im Allgemeinen, aber in diesem Fall ist die statische, was Sie wollen. Da der Sicherheitskontext einen Principal dem aktuell laufenden Thread zuordnet, greift der sicherste Code so direkt wie möglich auf den statischen Thread zu. Das Verbergen des Zugriffs hinter einer Wrapperklasse, die injiziert wird, bietet einem Angreifer mehr Angriffspunkte. Sie würden keinen Zugriff auf den Code benötigen (was sich schwer ändern würde, wenn das Jar signiert wäre), sie brauchen nur eine Möglichkeit, die Konfiguration zu überschreiben, was zur Laufzeit geschehen kann oder etwas XML in den Klassenpfad gleiten lässt. Selbst die Annotationsinjektion wäre mit externem XML übersteuerbar. Solch XML könnte das laufende System mit einem Rogue-Prinzipal infizieren.

8

Persönlich würde ich nur Powermock zusammen mit Mockito oder Easymock verwenden, um die statische SecurityContextHolder.getSecurityContext() in Ihrem Gerät/Integrationstest 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); 
     ... 
    } 
} 

Zwar ist es ziemlich viel Code Kesselblech hier also ein Authentifizierungsobjekt verspotten, eine Security verspotten die Authentifizierung zurückzukehren und schließlich die SecurityContextHolder verspotten die Security zu bekommen, aber seine sehr flexibel und ermöglicht es Ihnen, Einheit Testen Sie auf Szenarien wie null Authentifizierungsobjekte usw.ohne Ihren (nicht-Test) Code

97

Tun Sie es einfach die übliche Art und Weise zu ändern und es dann SecurityContextHolder.setContext() in Ihrer Test-Klasse einsetzen, zum Beispiel:

Controller:

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

Test:

+1

@Leonardo wo sollte 'Authentication a' im Controller hinzugefügt werden? Wie kann ich bei jeder Methodenaufrufung nachvollziehen? Ist es in Ordnung für "Frühling Weg", nur um es hinzuzufügen, anstatt zu injizieren? –

+0

Aber denken Sie daran, es wird nicht mit TestNG arbeiten, da SecurityContextHolder lokale Thread-Variable halten, so dass Sie diese Variable zwischen den Tests teilen ... –

1

Authentifizierung ist eine Eigenschaft eines Threads in der Serverumgebung auf die gleiche Weise wie eine Eigenschaft eines Prozesses in OS. Eine Bean-Instanz für den Zugriff auf Authentifizierungsinformationen zu haben, wäre ein unbequemer Konfigurations- und Verdrahtungsoverhead ohne irgendeinen Vorteil.

In Bezug auf Test-Authentifizierung gibt es mehrere Möglichkeiten, wie Sie Ihr Leben leichter machen können. Am liebsten mache ich eine benutzerdefinierte Annotation @Authenticated und prüfe den Ausführung Listener, der es verwaltet. Überprüfen Sie DirtiesContextTestExecutionListener für Inspiration.

0

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

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

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 
} 
2

Allgemeine

In der Zwischenzeit (seit Version 3.2 im Jahr 2013 dank SEC-2298) die Authentifizierung kann in MVC Methoden injiziert wird mit der Anmerkung @AuthenticationPrincipal:

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

Tests

In Ihrem Komponententest können Sie diese Methode natürlich direkt aufrufen. In Integrationstests org.springframework.test.web.servlet.MockMvc verwenden, können Sie org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user() verwenden Sie den Benutzer so zu injizieren:

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

Dies wird jedoch nur direkt füllen Sie das Security. Wenn Sie sicherstellen möchten, dass der Benutzer von einer Sitzung in Ihrem Test geladen ist, können Sie diese verwenden können:

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; 
     } 
    }; 
} 
16

Ohne die Beantwortung der Frage, wie Authentifizierung Objekte erstellen und zu injizieren, 4.0 Spring Security bietet einige begrüßenswerte Alternativen wenn es um das Testen geht. Die @WithMockUser Annotation ermöglicht den Entwickler einen Mock-Benutzer angeben (optional mit Behörden, Benutzername, Passwort und Rollen) in einer ordentlichen Art und Weise:

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

Es gibt auch die Option @WithUserDetails zu verwenden, aus den UserDetailsService ein UserDetails zurück zu emulieren , z.B

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

Weitere Details finden Sie in der @WithMockUser und die @WithUserDetails Kapitel in den Spring Security Referenz docs (von denen die obigen Beispiele, bei denen kopiert)