Quando si utilizza Spring Security, qual è il modo corretto di ottenere informazioni sul nome utente corrente (ovvero SecurityContext) in un bean?


288

Ho un'app Web Spring MVC che utilizza Spring Security. Voglio sapere il nome utente dell'utente attualmente connesso. Sto usando lo snippet di codice indicato di seguito. È questo il modo accettato?

Non mi piace avere una chiamata a un metodo statico all'interno di questo controller - che sconfigge l'intero scopo di Spring, IMHO. Esiste un modo per configurare l'app in modo che venga iniettato l'attuale SecurityContext o l'attuale autenticazione?

  @RequestMapping(method = RequestMethod.GET)
  public ModelAndView showResults(final HttpServletRequest request...) {
    final String currentUser = SecurityContextHolder.getContext().getAuthentication().getName();
    ...
  }

Perché non hai un controller (controller sicuro) come superclasse ottenere l'utente da SecurityContext e impostarlo come variabile di istanza all'interno di quella classe? In questo modo, quando si estende il controller sicuro, l'intera classe avrà accesso all'entità utente del contesto corrente.
Dehan de Croos,

Risposte:


259

Se stai usando Spring 3 , il modo più semplice è:

 @RequestMapping(method = RequestMethod.GET)   
 public ModelAndView showResults(final HttpServletRequest request, Principal principal) {

     final String currentUser = principal.getName();

 }

69

Molte cose sono cambiate nel mondo primaverile da quando è stata data una risposta a questa domanda. La primavera ha semplificato il coinvolgimento dell'utente corrente in un controller. Per altri bean, Spring ha adottato i suggerimenti dell'autore e semplificato l'iniezione di "SecurityContextHolder". Maggiori dettagli sono nei commenti.


Questa è la soluzione con cui ho finito. Invece di utilizzare SecurityContextHoldernel mio controller, voglio iniettare qualcosa che usa SecurityContextHoldersotto il cofano ma estrae quella classe singleton dal mio codice. Non ho trovato alcun modo per farlo se non rotolare la mia interfaccia, in questo modo:

public interface SecurityContextFacade {

  SecurityContext getContext();

  void setContext(SecurityContext securityContext);

}

Ora, il mio controller (o qualunque POJO) sarebbe simile a questo:

public class FooController {

  private final SecurityContextFacade securityContextFacade;

  public FooController(SecurityContextFacade securityContextFacade) {
    this.securityContextFacade = securityContextFacade;
  }

  public void doSomething(){
    SecurityContext context = securityContextFacade.getContext();
    // do something w/ context
  }

}

E, poiché l'interfaccia è un punto di disaccoppiamento, i test unitari sono semplici. In questo esempio uso Mockito:

public class FooControllerTest {

  private FooController controller;
  private SecurityContextFacade mockSecurityContextFacade;
  private SecurityContext mockSecurityContext;

  @Before
  public void setUp() throws Exception {
    mockSecurityContextFacade = mock(SecurityContextFacade.class);
    mockSecurityContext = mock(SecurityContext.class);
    stub(mockSecurityContextFacade.getContext()).toReturn(mockSecurityContext);
    controller = new FooController(mockSecurityContextFacade);
  }

  @Test
  public void testDoSomething() {
    controller.doSomething();
    verify(mockSecurityContextFacade).getContext();
  }

}

L'implementazione predefinita dell'interfaccia è simile alla seguente:

public class SecurityContextHolderFacade implements SecurityContextFacade {

  public SecurityContext getContext() {
    return SecurityContextHolder.getContext();
  }

  public void setContext(SecurityContext securityContext) {
    SecurityContextHolder.setContext(securityContext);
  }

}

E, infine, la configurazione Spring di produzione si presenta così:

<bean id="myController" class="com.foo.FooController">
     ...
  <constructor-arg index="1">
    <bean class="com.foo.SecurityContextHolderFacade">
  </constructor-arg>
</bean>

Sembra più che un po 'sciocco che Spring, un contenitore di iniezione di dipendenza di tutte le cose, non abbia fornito un modo per iniettare qualcosa di simile. Capisco sia SecurityContextHolderstato ereditato da acegi, ma comunque. Il fatto è che sono così vicini - se solo SecurityContextHolderavessi un getter per ottenere l' SecurityContextHolderStrategyistanza sottostante (che è un'interfaccia), potresti iniettarla. In effetti, ho persino aperto un problema di Jira in tal senso.

Un'ultima cosa: ho appena cambiato sostanzialmente la risposta che avevo qui prima. Controlla la cronologia se sei curioso ma, come mi ha sottolineato un collega, la mia risposta precedente non avrebbe funzionato in un ambiente multi-thread. Il sottostante SecurityContextHolderStrategyutilizzato da SecurityContextHolderè, per impostazione predefinita, un'istanza di ThreadLocalSecurityContextHolderStrategy, che memorizza SecurityContexts in a ThreadLocal. Pertanto, non è necessariamente una buona idea iniettare SecurityContextdirettamente un bean al momento dell'inizializzazione: potrebbe essere necessario recuperarlo ThreadLocalogni volta, in un ambiente multi-thread, quindi viene recuperato quello corretto.


1
Mi piace la tua soluzione: è un uso intelligente del supporto di metodo di fabbrica in primavera. Detto questo, questo funziona per te perché l'oggetto controller è nell'ambito della richiesta web. Se si modifica l'ambito del bean controller in modo errato, ciò si spezzerebbe.
Paul Morie,

2
I due commenti precedenti fanno riferimento a una vecchia risposta errata che ho appena sostituito.
Scott Bale,

12
È ancora la soluzione consigliata con l'attuale versione di Spring? Non riesco a credere che abbia bisogno di così tanto codice solo per recuperare solo il nome utente.
Ta Sas,

6
Se stai usando Spring Security 3.0.x, hanno implementato il mio suggerimento nel problema JIRA che ho registrato jira.springsource.org/browse/SEC-1188 in modo da poter ora iniettare l'istanza SecurityContextHolderStrategy (da SecurityContextHolder) direttamente nel tuo bean tramite standard Configurazione a molla
Scott Bale,

4
Si prega di vedere la risposta tsunade21. Spring 3 ora ti consente di utilizzare java.security.Principal come argomento del metodo nel tuo controller
Patrick

22

Sono d'accordo sul fatto che dover interrogare SecurityContext per l'attuale utente puzza, sembra un modo molto non-Spring per gestire questo problema.

Ho scritto una classe "helper" statica per affrontare questo problema; è sporco in quanto è un metodo globale e statico, ma ho pensato in questo modo se cambiassimo qualcosa legato alla sicurezza, almeno devo solo cambiare i dettagli in un unico posto:

/**
* Returns the domain User object for the currently logged in user, or null
* if no User is logged in.
* 
* @return User object for the currently logged in user, or null if no User
*         is logged in.
*/
public static User getCurrentUser() {

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

    if (principal instanceof MyUserDetails) return ((MyUserDetails) principal).getUser();

    // principal object is either null or represents anonymous user -
    // neither of which our domain User object can represent - so return null
    return null;
}


/**
 * Utility method to determine if the current user is logged in /
 * authenticated.
 * <p>
 * Equivalent of calling:
 * <p>
 * <code>getCurrentUser() != null</code>
 * 
 * @return if user is logged in
 */
public static boolean isLoggedIn() {
    return getCurrentUser() != null;
}

22
è lungo quanto SecurityContextHolder.getContext () e quest'ultimo è thread-safe poiché mantiene i dettagli di sicurezza in un threadLocal. Questo codice non mantiene alcuno stato.
opaco b

22

Per farlo apparire nelle tue pagine JSP, puoi usare il tag Spring Security Tag:

http://static.springsource.org/spring-security/site/docs/3.0.x/reference/taglibs.html

Per utilizzare uno qualsiasi dei tag, è necessario che il taglib di sicurezza sia dichiarato nel proprio JSP:

<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>

Quindi in una pagina jsp fai qualcosa del genere:

<security:authorize access="isAuthenticated()">
    logged in as <security:authentication property="principal.username" /> 
</security:authorize>

<security:authorize access="! isAuthenticated()">
    not logged in
</security:authorize>

NOTA: come menzionato nei commenti di @ SBerg413, dovrai aggiungere

d'uso espressioni = "true"

al tag "http" nella configurazione security.xml affinché funzioni.


Sembra che sia probabilmente il modo approvato da Spring Security!
Nick Spacek,

3
Affinché questo metodo funzioni, è necessario aggiungere use-expressions = "true" al tag http nella configurazione security.xml.
SBerg413

Grazie @ SBerg413, modificherò la mia risposta e aggiungerò il tuo importante chiarimento!
Brad Parks

14

Se si utilizza Spring Security ver> = 3.2, è possibile utilizzare l' @AuthenticationPrincipalannotazione:

@RequestMapping(method = RequestMethod.GET)
public ModelAndView showResults(@AuthenticationPrincipal CustomUser currentUser, HttpServletRequest request) {
    String currentUsername = currentUser.getUsername();
    // ...
}

Qui, CustomUserè un oggetto personalizzato che implementa UserDetailsrestituito da un'abitudine UserDetailsService.

Ulteriori informazioni sono disponibili nel capitolo @AuthenticationPrincipal dei documenti di riferimento di Spring Security.


13

Ottengo l'utente autenticato da HttpServletRequest.getUserPrincipal ();

Esempio:

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.support.RequestContext;

import foo.Form;

@Controller
@RequestMapping(value="/welcome")
public class IndexController {

    @RequestMapping(method=RequestMethod.GET)
    public String getCreateForm(Model model, HttpServletRequest request) {

        if(request.getUserPrincipal() != null) {
            String loginName = request.getUserPrincipal().getName();
            System.out.println("loginName : " + loginName );
        }

        model.addAttribute("form", new Form());
        return "welcome";
    }
}

Mi piace la tua soluzione. Per gli specialisti di primavera: è una soluzione sicura e buona?
marioosh,

Non è una buona soluzione. Otterrai nullse l'utente viene autenticato in modo anonimo ( http> anonymouselementi in Spring Security XML). SecurityContextHoldero SecurityContextHolderStrategyè il modo corretto.
Nowaker,

1
In modo che ho controllato se non null request.getUserPrincipal ()! = Null.
digz6666,

È nullo in Filter
Alex78191

9

In Spring 3+ hai le seguenti opzioni.

Opzione 1 :

@RequestMapping(method = RequestMethod.GET)    
public String currentUserNameByPrincipal(Principal principal) {
    return principal.getName();
}

Opzione 2 :

@RequestMapping(method = RequestMethod.GET)
public String currentUserNameByAuthentication(Authentication authentication) {
    return authentication.getName();
}

Opzione 3:

@RequestMapping(method = RequestMethod.GET)    
public String currentUserByHTTPRequest(HttpServletRequest request) {
    return request.getUserPrincipal().getName();

}

Opzione 4: fantasia: controlla questo per maggiori dettagli

public ModelAndView someRequestHandler(@ActiveUser User activeUser) {
  ...
}

1
Dal 3.2, arriva Spring-Security-Web @CurrentUserche funziona come l'abitudine @ActiveUserdal tuo link.
Mike Partridge,

@MikePartridge, non mi sembra di trovare quello che stai dicendo, nessun link ?? o maggiori informazioni ??
Azerafati,

1
Il mio errore: ho frainteso il javadoc di Spring Security AuthenticationPrincipalArgumentResolver . Viene mostrato un esempio che si avvolge @AuthenticationPrincipalcon @CurrentUserun'annotazione personalizzata . Dal 3.2 non è necessario implementare un risolutore di argomenti personalizzato come nella risposta collegata. Quest'altra risposta ha maggiori dettagli.
Mike Partridge,

5

Sì, la statica è generalmente negativa - in generale, ma in questo caso, la statica è il codice più sicuro che puoi scrivere. Poiché il contesto di sicurezza associa un principale al thread attualmente in esecuzione, il codice più sicuro accederà allo statico dal thread il più direttamente possibile. Nascondere l'accesso dietro una classe wrapper che viene iniettata fornisce a un attaccante più punti per attaccare. Non avrebbero bisogno di accedere al codice (che avrebbero difficoltà a cambiare se il jar fosse firmato), hanno solo bisogno di un modo per sovrascrivere la configurazione, che può essere fatta in fase di esecuzione o facendo scivolare un po 'di XML sul percorso di classe. Anche l'utilizzo dell'iniezione di annotazioni nel codice firmato sarebbe sovrascrivibile con XML esterno. Tale XML potrebbe iniettare il sistema in esecuzione con un'entità errata.


5

Vorrei solo fare questo:

request.getRemoteUser();

1
Potrebbe funzionare, ma non in modo affidabile. Da javadoc: "Se il nome utente viene inviato con ogni richiesta successiva dipende dal browser e dal tipo di autenticazione." - download-llnw.oracle.com/javaee/6/api/javax/servlet/http/…
Scott Bale

3
Questo è in realtà un modo valido e molto semplice per ottenere il nome utente remoto in un'applicazione Web di Spring Security. La catena di filtri standard include un SecurityContextHolderAwareRequestFilterche avvolge la richiesta e implementa questa chiamata accedendo a SecurityContextHolder.
Shaun the Sheep

4

Per l'ultima app Spring MVC che ho scritto, non ho iniettato il supporto SecurityContext, ma avevo un controller di base con due metodi di utilità correlati a questo ... isAuthenticated () & getUsername (). Internamente eseguono il metodo statico chiamato da te descritto.

Almeno allora è solo una volta se hai bisogno di rifattorizzare in seguito.


3

È possibile utilizzare l'approccio Spring AOP. Ad esempio, se si dispone di un servizio, è necessario conoscere l'entità corrente. È possibile introdurre un'annotazione personalizzata, ad esempio @Principal, che indica che questo servizio dovrebbe dipendere dall'entità principale.

public class SomeService {
    private String principal;
    @Principal
    public setPrincipal(String principal){
        this.principal=principal;
    }
}

Quindi, nel tuo consiglio, che ritengo debba estendere MethodBeforeAdvice, controlla che quel particolare servizio abbia l'annotazione @Principal e inietti il ​​nome Principal, oppure impostalo invece su 'ANONYMOUS'.


Devo accedere a Principal all'interno della classe di servizio. Potresti pubblicare un esempio completo su github? Non conosco la primavera AOP, quindi la richiesta.
Rakesh Waghela,

2

L'unico problema è che, anche dopo l'autenticazione con Spring Security, il bean utente / principale non esiste nel contenitore, quindi l'iniezione di dipendenza sarà difficile. Prima di utilizzare Spring Security, dovevamo creare un bean con ambito sessione che avesse l'attuale Principal, iniettarlo in un "AuthService" e quindi iniettare quel servizio nella maggior parte degli altri servizi nell'applicazione. Quindi quei Servizi chiamerebbero authService.getCurrentUser () per ottenere l'oggetto. Se hai un posto nel tuo codice in cui ottieni un riferimento allo stesso principale nella sessione, puoi semplicemente impostarlo come proprietà sul tuo bean con ambito di sessione.


1

Prova questo

Autenticazione autenticazione = SecurityContextHolder.getContext (). GetAuthentication ();
Stringa userName = authentication.getName ();


3
Chiamare il metodo statico SecurityContextHolder.getContext () è esattamente ciò di cui mi lamentavo nella domanda originale. Non hai risposto a niente.
Scott Bale,

2
È, tuttavia, esattamente ciò che la documentazione raccomanda: static.springsource.org/spring-security/site/docs/3.0.x/… Quindi cosa stai realizzando evitandolo? Stai cercando una soluzione complicata a un semplice problema. Nella migliore delle ipotesi, ottieni lo stesso comportamento. Nel peggiore dei casi, si ottiene un bug o un buco nella sicurezza.
Bob Kerns,

2
@BobKerns Per i test, è più pulito essere in grado di iniettare l'autenticazione anziché inserirla in un thread locale.

1

La migliore soluzione se stai usando Spring 3 e hai bisogno dell'entità autenticata nel tuo controller è fare qualcosa del genere:

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;

    @Controller
    public class KnoteController {
        @RequestMapping(method = RequestMethod.GET)
        public java.lang.String list(Model uiModel, UsernamePasswordAuthenticationToken authToken) {

            if (authToken instanceof UsernamePasswordAuthenticationToken) {
                user = (User) authToken.getPrincipal();
            }
            ...

    }

1
Perché stai facendo quell'istanza di UsernamePasswordAuthenticationToken verificato quando il parametro è già di tipo UsernamePasswordAuthenticationToken?
Scott Bale,

(authToken instanceof UsernamePasswordAuthenticationToken) è equivalente funzionale di if (authToken! = null). Quest'ultimo potrebbe essere un po 'più pulito, ma per il resto non c'è differenza.
Segna il

1

Sto usando l' @AuthenticationPrincipalannotazione nelle @Controllerclassi e in @ControllerAdvicerquelle annotate. Ex.:

@ControllerAdvice
public class ControllerAdvicer
{
    private static final Logger LOGGER = LoggerFactory.getLogger(ControllerAdvicer.class);


    @ModelAttribute("userActive")
    public UserActive currentUser(@AuthenticationPrincipal UserActive currentUser)
    {
        return currentUser;
    }
}

Dov'è UserActivela classe che utilizzo per i servizi degli utenti registrati e da cui si estende org.springframework.security.core.userdetails.User. Qualcosa di simile a:

public class UserActive extends org.springframework.security.core.userdetails.User
{

    private final User user;

    public UserActive(User user)
    {
        super(user.getUsername(), user.getPasswordHash(), user.getGrantedAuthorities());
        this.user = user;
    }

     //More functions
}

Davvero facile.


0

Definisci Principalcome dipendenza nel metodo del controller e spring invierà l'utente autenticato corrente nel tuo metodo al momento dell'invocazione.


-2

Mi piace condividere il mio modo di supportare i dettagli dell'utente sulla pagina dei freemarker. Tutto è molto semplice e funziona perfettamente!

Devi solo posizionare il Richiesta di autenticazione su default-target-url(pagina dopo il form-login) Questo è il mio metodo Controler per quella pagina:

@RequestMapping(value = "/monitoring", method = RequestMethod.GET)
public ModelAndView getMonitoringPage(Model model, final HttpServletRequest request) {
    showRequestLog("monitoring");


    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    String userName = authentication.getName();
    //create a new session
    HttpSession session = request.getSession(true);
    session.setAttribute("username", userName);

    return new ModelAndView(catalogPath + "monitoring");
}

E questo è il mio codice ftl:

<@security.authorize ifAnyGranted="ROLE_ADMIN, ROLE_USER">
<p style="padding-right: 20px;">Logged in as ${username!"Anonymous" }</p>
</@security.authorize> 

E questo è tutto, il nome utente apparirà su ogni pagina dopo l'autorizzazione.


Grazie per aver cercato di rispondere, ma l'uso del metodo statico SecurityContextHolder.getContext () è esattamente ciò che volevo evitare e il motivo per cui ho posto questa domanda in primo luogo.
Scott Bale,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.