Frage

Meine Firma wurde die Bewertung Spring MVC, um zu bestimmen, ob wir es in einem unserer nächsten Projekte verwenden sollten. Bisher Ich liebe, was ich gesehen habe, und jetzt bin ich einen Blick auf das Spring Security-Modul nehmen, um festzustellen, ob es etwas ist, können wir / verwenden sollten.

Unsere Sicherheitsanforderungen sind ziemlich einfach; ein Benutzer muss nur bestimmte Teile der Website (wie bekommen Informationen über ihr Konto) zugreifen zu können, einen Benutzernamen und ein Passwort zur Verfügung stellen zu können; und es gibt eine Handvoll Seiten auf der Website (FAQs, Support usw.), wo ein anonymer Benutzer sollte Zugang gegeben werden.

In der Prototyp-I zu schaffen habe, habe ich ein „LoginCredentials“ Objekt speichert (die nur Benutzername und Kennwort enthält) in Session für einen authentifizierten Benutzer; einige der Controller überprüfen, um zu sehen, ob dieses Objekt in der Sitzung ist eine Referenz auf den angemeldeten Benutzername, zum Beispiel zu bekommen. Ich suche diese home-grown-Logik mit Spring Security ersetzen statt, die den schönen Vorteil der Entfernung jeglicher Art haben würde „wie wir eingeloggte Benutzer den Überblick?“ und „Wie können wir Benutzer authentifizieren?“ von meinem Controller / Business-Code.

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

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

... das scheint sehr un-Spring wie als dieses Objekt ein (global) Singleton, in einer Art und Weise.

Meine Frage ist: Wenn dies der normale Weg ist, Informationen über die authentifizierten Benutzer in Spring Security zuzugreifen, was ist die akzeptierte Möglichkeit, ein Authentifizierungsobjekt in den Security zu injizieren, so dass es für meine Unit-Tests, wenn das Gerät verfügbar ist Tests erfordern einen authentifizierten Benutzer?

Muß ich dies in der Initialisierungsmethode jeden Testfalls verkabeln?

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

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

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

War es hilfreich?

Lösung

Das Problem ist, dass Spring Security nicht die Authentifizierung macht Objekt zur Verfügung wie eine Bohne in den Behältern, so gibt es keine Möglichkeit, leicht zu injizieren oder aus dem Kasten heraus autowire.

Bevor wir beginnen Spring Security zu verwenden, würden wir eine Session-scoped Bean im Container erstellen den Auftraggeber zu speichern, injizieren diese in eine „AuthenticationService“ (Singleton) und dann diese Bohne in andere Dienste injizieren, die Kenntnisse über die benötigten Stromhaupt.

Wenn Sie ein eigene Authentifizierungsdienst implementieren, könnte man im Grunde das gleiche tun: Erstellen Sie eine Session-scoped Bohne mit einer „Kapital“ Eigenschaft, injiziert diese in Ihren Authentifizierungsdienst, hat den Auth-Dienst die Eigenschaft auf erfolgreiche Auth eingestellt und dann den auth-Service zu anderen Bohnen zur Verfügung stellen, wie Sie es brauchen.

Ich würde nicht so schlecht fühlen SecurityContextHolder verwenden. obwohl. Ich weiß, dass es eine statische / Singleton und dass Frühling schreckt solche Dinge verwenden, aber ihre Umsetzung kümmert sich entsprechend abhängig von der Umgebung zu verhalten: Sitzung scoped-in einem Servlet-Container, faden scoped in einem JUnit-Test usw. Die eigentliche limitierende Faktor Singleton von a ist, wenn es eine Implementierung bereitstellt, die an unterschiedliche Umgebungen unflexibel ist.

Andere Tipps

Tu es einfach die übliche Art und Weise und dann einfügen SecurityContextHolder.setContext() in Ihrer Test-Klasse, zum Beispiel:

Controller:

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

Test:

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);

Sie ganz richtig sind betroffen zu sein - statische Methodenaufrufe sind besonders problematisch für Unit-Tests, da Sie nicht einfach Ihre Abhängigkeiten spotten kann. Was ich Ihnen zeigen, wie der Frühling IoC Behälter tun, um die schmutzige Arbeit für Sie zu lassen, Sie mit ordentlich, überprüfbarem Code zu verlassen. SecurityContextHolder ist ein Framework-Klasse und während es in Ordnung sein kann für Ihre Low-Level-Sicherheitscode an sie gebunden zu sein, möchten Sie wahrscheinlich eine sauberere Schnittstelle zu Ihrem UI-Komponenten (d-Controller) verfügbar machen.

cliff.meyers erwähnte einen Weg darum herum - Ihre eigene „Kapital“ Art erstellen und eine Instanz in den Verbraucher injizieren. Der Frühling << a href = "http://static.springframework.org/spring/docs/2.5.x/reference/beans.html#beans-factory-scopes-other-injection" rel = "noreferrer"> AOP: scoped-Proxy /> Tag in 2.x mit einer Anfrage Anwendungsbereich Bean Definition kombiniert eingeführt, und die Fabrik-Methode Unterstützung kann das Ticket für die meisten lesbaren Code sein.

Es könnte funktionieren wie folgt:

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? In der Tat hatten Sie wahrscheinlich die meisten dies bereits zu tun. Als nächstes wird in Ihrem Bean Kontext einer Anfrage-scoped Bohne definieren den Haupt 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 wird jedes Mal eine neue HTTP-Anforderung kommt und alle Verweise auf die Eigenschaft current aufgerufen werden, korrekt aufgelöst werden. Jetzt Unit-Tests werden trivial:

protected void setUp() {
    // existing init code

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

Hope, das hilft!

Ohne die Beantwortung der Frage, wie Authentifizierung Objekte erstellen und zu injizieren, 4.0 Spring Security bietet einige begrüßenswerte Alternativen, wenn es um Tests kommt. 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 verwendet ein UserDetails zu emulieren vom UserDetailsService zurückgegeben, z.

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

Weitere Informationen finden Sie in der @ WithMockUser und die @ WithUserDetails Kapitel in dem Spring Security Referenz docs (von denen die obigen Beispiele wurden kopiert)

Persönlich würde ich nur PowerMock verwende zusammen mit Mockito oder EasyMock die statische SecurityContextHolder.getSecurityContext () in der Einheit / Integrationstest beispiel zu verspotten.

@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 Kesselblech Code hier also ein Authentifizierungsobjekt verspotten, eine Security verspotten die Authentifizierung und schließlich verspotten die SecurityContextHolder zurückzukehren, um die Security jedoch seine sehr flexibel und ermöglicht es Ihnen, Unit-Test-Szenarien zu erhalten wie null Authentifizierung Objekte usw. ohne Ihre (nicht-Test) Code ändern zu müssen

ein statische in diesem Fall ist der beste Weg, sicheren Code zu schreiben.

Ja, Statik ist in der Regel schlecht - in der Regel, aber in diesem Fall ist die statisch ist, was Sie wollen. Da der Sicherheitskontext eines Principals mit dem aktuell laufenden Thread zuordnet, wäre die sicherste Code die statische aus dem so direkt wie möglich Thread zuzugreifen. Ausblenden der Zugang hinter einer Wrapper-Klasse, die injiziert wird, bietet einen Angreifer mit mehr Punkten zum Angriff. Sie würden keinen Zugriff auf den Code benötigen (was sie eine harte Zeit zu ändern, wenn das Glas unterzeichnet hätte), sie müssen nur einen Weg, um die Konfiguration zu überschreiben, die zur Laufzeit durchgeführt werden können oder eine XML auf den Classpath rutscht. Auch würde mit Annotations Injektion mit externen XML-overridable sein. Solche XML das laufende System mit einem Schelm Haupt injizieren könnte.

Ich fragte die gleiche Frage mich über hier und erzielen nur eine Antwort, die ich vor kurzem gefunden. Kurze Antwort ist: injizieren eine SecurityContext, und beziehen sich nur im Frühling config SecurityContextHolder die SecurityContext zu erhalten

Ich würde einen Blick auf Spring abstrakten Testklassen nehmen und Mock-Objekte, die über hier . Sie bieten eine leistungsfähige Möglichkeit der Auto-Wiring der Spring-verwalteten Objekte machen Modul- und Integrationstests erleichtern.

Allgemein

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

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

Tests

In Ihrem Unit-Test können Sie natürlich diese Methode 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 die Security füllen. Wenn Sie sicherstellen möchten, dass der Benutzer von einer Sitzung in Ihrem Test geladen ist, können Sie diese 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;
        }
    };
}

Die Authentifizierung ist eine Eigenschaft eines Threads in Server-Umgebung in der gleichen Weise wie es eine Eigenschaft eines Prozesses in OS ist. eine Bean-Instanz Nachdem Authentifizierungsinformationen für den Zugriff wäre unbequem Konfiguration und Verdrahtungsaufwand ohne Nutzen.

In Bezug auf Test Authentifizierung gibt es mehrere Möglichkeiten, wie Sie Ihr Leben leichter machen können. Mein Favorit ist eine benutzerdefinierte Anmerkung @Authenticated und Testausführung Zuhörer zu machen, die er verwaltet. Prüfen DirtiesContextTestExecutionListener für Inspiration.

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

Natürlich bin ich bereit, diese neuen Funktionen in Spring Security 4.0 zu sehen, dass unsere Tests 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 
}
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top