Domanda

La mia azienda ha valutato Spring MVC per determinare se dovremmo usare in uno dei nostri prossimi progetti. Finora mi piace quello che ho visto, e in questo momento sto prendendo uno sguardo al modulo di Primavera di sicurezza per determinare se si tratta di qualcosa che possiamo dovremmo usare /.

I nostri requisiti di sicurezza sono piuttosto essenziali; un utente ha solo bisogno di essere in grado di fornire un nome utente e una password per poter accedere ad alcune parti del sito (ad esempio, per ottenere informazioni circa il loro conto); e ci sono una manciata di pagine sul sito (domande frequenti, supporto, ecc) dove un utente anonimo dovrebbe avere accesso.

Nel prototipo ho creare, ho la memorizzazione di un oggetto "LoginCredentials" (che contiene solo nome utente e password) nella sessione per un utente autenticato; alcuni dei controllori di controllare per vedere se questo oggetto è in sessione per ottenere un riferimento al nome utente connesso, ad esempio. Sto cercando di sostituire questa logica di produzione propria con Primavera di sicurezza, invece, che avrebbe il bel vantaggio di eliminare qualsiasi tipo di "come si fa a tenere traccia gli utenti registrati?" e "come possiamo autenticare gli utenti?" dal mio controller codice / incassi.

Sembra Primavera di sicurezza fornisce un oggetto (per-thread) "contesto" per essere in grado di accedere al nome utente informazioni / principale da qualsiasi punto della app ...

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

... che sembra molto poco primavera come come questo oggetto è un Singleton (globale), in un certo senso.

La mia domanda è questa: se questo è il modo standard per accedere alle informazioni relative all'utente autenticato in Primavera di sicurezza, qual è il modo accettato di iniettare un oggetto di autenticazione nella SecurityContext in modo che sia disponibile per mio test di unità quando l'unità test richiedono un utente autenticato?

Devo legare questo in su nel metodo di inizializzazione di ciascun caso di test?

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

Questo sembra eccessivamente verbose. C'è un modo più semplice?

Il SecurityContextHolder oggetto stesso sembra molto poco primaverile ...

È stato utile?

Soluzione

Il problema è che Spring Security non rendere l'oggetto autenticazione disponibili come fagiolo nel contenitore, quindi non c'è modo per iniettare o autowire fuori dalla scatola facilmente.

Prima di iniziare ad utilizzare Primavera di sicurezza, vorremmo creare un bean di sessione con ambito nel contenitore per memorizzare il Principal, iniettare questo in un "AuthenticationService" (singolo) e poi iniettare questo fagiolo in altri servizi che avevano bisogno la conoscenza del Principal corrente.

Se si implementa il proprio servizio di autenticazione, si potrebbe fondamentalmente fare la stessa cosa: creare una session bean con ambito con una proprietà "principale", iniettare questo nella vostra servizio di autenticazione, avere il servizio di autenticazione impostare la proprietà su autenticazione di successo , e quindi rendere il servizio di autenticazione a disposizione di altri fagioli come avete bisogno.

Non mi sentirei troppo male sull'utilizzo SecurityContextHolder. anche se. So che è una statica / Singleton e che la primavera scoraggia l'utilizzo di tali cose, ma la loro attuazione si prende cura di comportarsi in modo appropriato a seconda dell'ambiente: sessione ambito in un servlet container, filo-ambito in un test JUnit, etc. Il vero fattore limitante di un Singleton è quando si prevede un'implementazione che è inflessibile ad ambienti differenti.

Altri suggerimenti

Basta farlo nel modo solito e poi inserirla utilizzando SecurityContextHolder.setContext() nella classe di test, per esempio:

Controller:

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

Prova:

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

Lei ha perfettamente ragione di essere preoccupati - chiamate a metodi statici sono particolarmente problematico per unit testing come non si può prendere in giro facilmente le dipendenze. Quello che sto per mostrarvi è come lasciare il contenitore IoC Primavera fare il lavoro sporco per voi, lasciandovi con pulito, codice verificabile. SecurityContextHolder è una classe quadro e mentre può essere ok per il codice di sicurezza di basso livello per essere legato ad esso, probabilmente si desidera esporre un'interfaccia più ordinato ai componenti dell'interfaccia utente (cioè controllori).

cliff.meyers menzionati in un modo intorno ad esso - creare un tipo di "principale" e iniettare un'istanza in consumatori. La primavera << a href = "http://static.springframework.org/spring/docs/2.5.x/reference/beans.html#beans-factory-scopes-other-injection" rel = "noreferrer"> AOP: ambito-proxy /> tag introdotto nel 2.x combinato con una definizione di fagioli richiesta di campo di applicazione, e l'appoggio di fabbrica-metodo può essere il biglietto per il codice più leggibile.

Si potrebbe funzionare come segue:

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
}

Niente di complicato finora, giusto? In realtà probabilmente si doveva fare la maggior parte di questo già. Successivamente, nel contesto di bean definire un chicco di richiesta con ambito per contenere principale:

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

Grazie alla magia del AOP: tag ambito-proxy, il metodo getUserDetails statici saranno chiamati ogni volta che una nuova richiesta HTTP entra e tutti i riferimenti alla proprietà currentUser verrà risolto correttamente. Ora unit testing diventa banale:

protected void setUp() {
    // existing init code

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

Spero che questo aiuti!

Senza rispondere alla domanda su come creare e iniettare oggetti di autenticazione, Primavera Security 4.0 fornisce alcune alternative di benvenuto quando si tratta di test. Il @WithMockUser di annotazione consente allo sviluppatore di specificare un utente finto (con le autorità opzionali, nome utente, password e ruoli) in un modo pulito:

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

C'è anche la possibilità di utilizzare @WithUserDetails per emulare un UserDetails restituito dal UserDetailsService, per es.

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

Maggiori dettagli possono essere trovati nel @ WithMockUser e la @ WithUserDetails capitoli nei documenti di riferimento della molla di sicurezza (da cui gli esempi sopra riportati sono stati copiati)

Personalmente vorrei solo usare Powermock con Mockito o EasyMock per deridere la statica SecurityContextHolder.getSecurityContext () nella tua unit test / integrazione per es.

@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);
        ...
    }
}

Certo c'è un po 'di codice piastra caldaia qui cioè deridere un oggetto di autenticazione, deridere un SecurityContext per restituire l'autenticazione e infine giro il SecurityContextHolder per ottenere il SecurityContext, tuttavia la sua molto flessibile e consente di unit test per scenari come oggetti di autenticazione nulli, ecc, senza dover cambiare la vostra (non di prova) codice

Utilizzo di una statica, in questo caso è il modo migliore per scrivere codice sicuro.

Sì, statica sono generalmente male - in generale, ma in questo caso, la statica è ciò che si desidera. Poiché il contesto di protezione associa un principale con il filo attualmente in esecuzione, il codice più sicuro sarebbe accedere al statica dalla filettatura più direttamente possibile. Nascondere l'accesso dietro una classe wrapper che viene iniettato fornisce un attaccante con più punti di attacco. Non avrebbero bisogno di accedere al codice (che avrebbero un momento difficile cambiare se il barattolo è stato firmato), hanno solo bisogno di un modo per ignorare la configurazione, che può essere fatto in fase di esecuzione o scivolare un po 'di XML sul classpath. Anche usando iniezione annotazione sarebbe superabile con XML esterno. Tale XML potrebbe iniettare il sistema in esecuzione con un principale canaglia.

Ho chiesto la stessa domanda che mi sopra qui , e appena pubblicato una risposta che ho trovato di recente. risposta breve è: iniettare un SecurityContext, e si riferiscono SecurityContextHolder solo nella vostra primavera config per ottenere il <=>

Vorrei dare un'occhiata a classi di test astratte di primavera e oggetti mock che vengono parlato qui . Essi forniscono un modo potente di auto-cablaggio vostra primavera oggetti gestiti dell'unità di creazione e l'integrazione di test più facili.

Generale

Nel frattempo (dalla versione 3.2, nel 2013, grazie a SEC-2298) l'autenticazione può essere iniettato in metodi MVC usando l'annotazione @ AuthenticationPrincipal :

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

Test

Nel vostro test di unità è possibile, ovviamente, chiamare direttamente questo metodo. Nel test di integrazione utilizzando org.springframework.test.web.servlet.MockMvc è possibile utilizzare org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user() per iniettare l'utente in questo modo:

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

In questo modo però basta compilare direttamente il SecurityContext. Se si vuole fare in modo che l'utente viene caricato da una sessione nel test, è possibile utilizzare questo:

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

L'autenticazione è una proprietà di un filo in ambiente server nello stesso modo in cui si tratta di una proprietà di un processo in OS. Avere un'istanza di bean per accedere alle informazioni di autenticazione sarebbe configurazione scomodo e onere di cablaggio, senza alcun beneficio.

Per quanto riguarda l'autenticazione di prova ci sono diversi modi come si può rendere la vita più facile. Il mio preferito è quello di fare un'annotazione personalizzata @Authenticated e l'esecuzione di test ascoltatore, che lo gestisce. Controllare DirtiesContextTestExecutionListener per l'ispirazione.

Dopo un bel po 'di lavoro che è stato in grado di riprodurre il comportamento desiderato. Avevo emulato il login attraverso MockMvc. E 'troppo pesante per la maggior parte dei test di unità, ma utile per i test di integrazione.

Naturalmente io sono disposto a vedere quelle nuove funzionalità di primavera Security 4.0 che renderà i nostri test più facile.

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 
}
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top